feat: harness-owned reading executor (first specialized tool) #64
No reviewers
Labels
No labels
Agent
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
joshtronic/igor!64
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/harness-reading-executor"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
First concrete step toward the hybrid architecture: stop relying
on Claude Code's agent loop for procedural work, build dedicated
tools that the harness orchestrates.
Two new scripts:
bin/agent-read.sh
Low-level: fetches URL via curl, sends HTML + identity-laden
system prompt to the Anthropic API (Sonnet by default), parses
the JSON response into {title, journal, url}, outputs that on
stdout. Self-contained -- can be invoked manually for testing.
bin/discretionary-read.sh
Orchestrator: discovers a candidate URL from the discovery page
(default joshtronic.com/links, override via env), dedupes against
the reading log (no re-reads), calls agent-read.sh, writes the
journal entry to .igor/IGOR_JOURNAL.md in the worktree, and
appends a "domain -- title -- URL" line to the reading log under
today's date. Idempotent.
Tick.sh wiring:
Inside the discretionary block, after worktree setup, roll dice
against IGOR_HARNESS_READING_RATE (default 0, off). When it lands,
invoke discretionary-read.sh and skip the Claude Code path
entirely. The existing journal-write/commit flow picks up the
IGOR_JOURNAL.md the executor wrote, same as it would for a
Claude-written one.
Net effect when the flag is on:
on Claude to update it (yesterday's mikekey.com / today's
"Replacing Notion" re-reads were both caused by Claude
skipping that step)
Claude to pick it
Three new env vars in .env.example:
IGOR_HARNESS_READING_RATE probability (default 0)
IGOR_READING_DISCOVERY_URL where candidates come from
IGOR_MODEL_READING voice-quality model for journals
Future: same pattern extends to drafting posts (agent-draft.sh),
running maintenance audits, and other procedural work. This PR
is the proof of concept.
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
First concrete step toward the hybrid architecture: stop relying on Claude Code's agent loop for procedural work, build dedicated tools that the harness orchestrates. Two new scripts: - bin/agent-read.sh <url> Low-level: fetches URL via curl, sends HTML + identity-laden system prompt to the Anthropic API (Sonnet by default), parses the JSON response into {title, journal, url}, outputs that on stdout. Self-contained -- can be invoked manually for testing. - bin/discretionary-read.sh <worktree> Orchestrator: discovers a candidate URL from the discovery page (default joshtronic.com/links, override via env), dedupes against the reading log (no re-reads), calls agent-read.sh, writes the journal entry to .igor/IGOR_JOURNAL.md in the worktree, and appends a "domain -- title -- URL" line to the reading log under today's date. Idempotent. Tick.sh wiring: Inside the discretionary block, after worktree setup, roll dice against IGOR_HARNESS_READING_RATE (default 0, off). When it lands, invoke discretionary-read.sh and skip the Claude Code path entirely. The existing journal-write/commit flow picks up the IGOR_JOURNAL.md the executor wrote, same as it would for a Claude-written one. Net effect when the flag is on: - One direct API call instead of a full agent loop - One Haiku/Sonnet API call instead of Claude Code's many turns - Reading log gets URL logged automatically -- no more relying on Claude to update it (yesterday's mikekey.com / today's "Replacing Notion" re-reads were both caused by Claude skipping that step) - Fully predictable: harness owns shape c instead of asking Claude to pick it Three new env vars in .env.example: IGOR_HARNESS_READING_RATE probability (default 0) IGOR_READING_DISCOVERY_URL where candidates come from IGOR_MODEL_READING voice-quality model for journals Future: same pattern extends to drafting posts (agent-draft.sh), running maintenance audits, and other procedural work. This PR is the proof of concept. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>Earlier commit on this branch introduced 3 env vars when only 1 was needed. Cleaning up: DROPPED: - IGOR_READING_DISCOVERY_URL -- was a single configurable URL to scrape candidate links from. Wrong shape: the actual sources Igor uses are a small fixed set (Josh's blog, Jen's blog, HN, prior sources from his reading log). Belongs in the harness decision tree, not user config. - IGOR_MODEL_READING -- speculative override, no real demand. The reader script now just uses IGOR_MODEL. KEPT: - IGOR_HARNESS_READING_RATE -- the actual feature toggle. Earns its keep. Rewrote discretionary-read.sh's URL discovery as a decision tree with four named strategies that match the sources Igor actually reads: 25% joshtronic.com -- Josh's blog 15% thatgirljen.com -- Jen's blog 30% Hacker News front page 30% Prior sources -- domains in the reading log Roll dice -> primary strategy + fallback order. If a strategy comes up empty (Jen hasn't posted, HN is down, no fresh URLs), fall through to the next in the order rather than failing the tick. Each strategy fetches an index page, extracts links specific to that source, dedupes against the reading log, and picks one at random. Kagi Small Web deferred (needs auth-walled API). Logs which strategy fired ("discretionary-read: selected via joshtronic: <URL>") so you can see the distribution in journalctl over time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>The previous version had four named source strategies hardcoded in bash (joshtronic, thatgirljen, hackernews, prior_source). That meant adding a new source required a harness PR. Wrong scope -- the source list is Igor's curated content, not harness infrastructure. Now: parse brain/memories/reading/sources.md (added in the companion brain PR feat/reading-sources). Each line matching `- <weight> -- <url> -- <label>` is a source with that integer weight. Sources with weight 0 are skipped, so a source can stay listed for reference while disabled. Algorithm: 1. Parse sources.md -> list of (weight, url) pairs. 2. Weighted random sample to pick a primary source. 3. Fetch that source, extract links, dedupe against reading log, pick one at random. 4. If empty (offline, all read, parse failure), drop that source and resample. Iterate until we find a fresh URL or run out of sources. What this gains: - Igor can edit sources.md himself when he discovers a new source or grows tired of an old one - Adding sources doesn't require a harness PR - The "prior sources" strategy collapses into the file -- domains worth returning to just go in the list - Weights are auditable in one place The harness fails loudly if sources.md doesn't exist (brain not cloned yet, sources.md not committed). Caller wraps in error handling so the tick falls through to claude-code path rather than breaking. Companion brain change: feat/reading-sources -- seeds sources.md with Josh's blog, Jen's blog, HN, and the friends/inspo from joshtronic.com/links.