feat: harness-owned reading executor (first specialized tool) #64

Merged
joshtronic merged 3 commits from feat/harness-reading-executor into master 2026-05-21 22:45:22 +00:00
Owner

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:

  • 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

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>
feat: harness-owned reading executor (first specialized tool)
All checks were successful
Lint / check-sync (push) Successful in 5s
Lint / check-sync (pull_request) Successful in 4s
95e78a2aab
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>
fix: scope creep -- drop 2 unnecessary env vars, build the decision tree
All checks were successful
Lint / check-sync (push) Successful in 4s
Lint / check-sync (pull_request) Successful in 4s
15badd27c6
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>
feat: read source list from brain/memories/reading/sources.md
All checks were successful
Lint / check-sync (push) Successful in 4s
Lint / check-sync (pull_request) Successful in 4s
682b93b557
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.
joshtronic deleted branch feat/harness-reading-executor 2026-05-21 22:45:22 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
joshtronic/igor!64
No description provided.