Two accounts, two machines, one spec
Coordinating a clean-room rebuild series across a laptop and a FreeBSD jail: the roles, the channel, the failures, and what it cost

Table of Contents

Written the night of the first day. The project — a private repository simulating a commerce and fulfilment estate behind a chaos proxy — is the subject; the coordination around it is the point. Sources: the project's own runner notes, coordination protocol and nomenclature file, the coordinator's verdict log, and the governing spec's review ledger, all read locally. Numbers are as of 01:25 ET on 2026-09-13.

This is one run. Nothing here is a general finding about how agent fleets should be coordinated; it is what happened on one day, measured where it could be measured and named as unmeasured where it could not. The method the run was executing is described separately in Rebuilding from the spec.

The shape

Three roles, two machines, two accounts.

  • Runner: a Claude Code session on a macOS laptop, on the user's own Max account, which was at 94% of its weekly limit when the day started. Its standing rule: relay, log one line, stop. No subagents, no builds.
  • Coordinator: a Claude Code session inside a Bastille jail on a FreeBSD 15.1 host, on a second Max account. Writes the spec and every contract, dispatches workers, gates deliveries, never writes a unit itself.
  • Workers: one subagent per unit per rebuild, each in its own worktree, each seeing only its contract and a builder CLAUDE.md. Sealed: no worker reads another unit, the reference implementation, or the repo root.

The channel between the two machines is tmux over SSH: the runner sends keystrokes into the jail's session and reads the pane back. git is the durable channel (main, git notes, a ledger in the spec). There was no agent-to-agent messaging across the boundary and none was needed.

What was built, and by what

A rebuild series: 000 is the Python reference; 001 to 008 are one language each (Python, JavaScript, Go, Ruby, Erlang, C, Guile, Clojure on babashka), each rebuilding every unit from the same contracts, judged by the same oracle. The final is polyglot, per unit the language of the system it stands in for, Clojure only for the visualization.

By the end of day one, 001 had fourteen of fifteen units accepted in isolation, each with an in-language test suite, and the coordinator had a ledger of 98 entries: things the text got wrong and the build found.

Cost

Measured from the transcripts, deduplicated by API message id, taking the maximum output per id because streamed lines under one id grow it (the audit script's first version undercounted output by up to 440×, which the coordinator caught by knowing a 542-line file cannot cost 105 tokens).

where calls output cache read
15 unit builds for 001 (haiku, some sonnet) 1,849 1.43 M 179 M
7 review agents (sonnet)   240 k 29 M
coordinator session (Opus 5) 573 813 k 252 M
runner session (Fable 5.1) 330 247 k 98.5 M

Cache read runs about 126× output on every row. A worker re-reads its contract, the rules and its own files on every turn, so contract size is what a unit costs, not the code it writes. The runner's own $17.80 by mid-afternoon was 78% attributable to context above 150k and the two research subagents it ran before the no-subagents rule.

Agents used, and the model ladder

  • Workers on haiku. A unit returned twice escalates to sonnet, recorded in the ledger as a signal about the contract. Four escalated: oracle, otelsink, pickersim, tally. Every escalation was accepted on the first delivery at the larger model, and in every case the clause that would have caught the defect did not exist until the gate run that found it.
  • Opus 4.7 was wanted as an intermediate and turned out unreachable: the Agent tool's model parameter is a closed enum of four aliases. Tested, not assumed.
  • One unit (taxsim) was built off-policy on fable, noticed only by the cost audit, and rebuilt on haiku from the contract alone: 30 fixture rows exact to 1e-9. That is the result worth keeping.
  • Review agents (sonnet) walked each unit's flow from its own berth; an L7 architect reviewed boundaries and then full semantics; two renames came out of it, both because a name described an implementation detail or the wrong side of a device/system boundary.

Failures worth keeping

  • The instrument was wrong before the worker was. Twice in the first hour a gate reported green with nothing checked: an awk exit code that never reached the shell, a self-test that skipped five mutations at exit 0. The fix that stuck: sabotage. Replace each rule with a stub that reports success and require the checker to go red and name it.
  • A worker delivered a byte-identical file with a report describing three fixes. It had branched from a main where the file did not exist. Rule added: diff against the returned sha before opening a PR; an empty diff is no delivery.
  • A suite written alongside a fix tests the fix. tally's second delivery fixed the wedge and regressed a clause that had passed.
  • Contracts fail where they are underspecified, exactly there. otelsink listed three 400 cases; the decoder satisfied them and accepted garbage.
  • A non-goal written against a word is a trap. Three times a non-goal contradicted the clause above it ("no write endpoints" beside a required POST).
  • Renames are cheap only before anyone builds. Two renames in a day cost zero deliveries because the coordinator checked the claim rather than taking it.

Channel failures, all on the runner's side

  • Every SSH connection to the host had been riding the user's ControlMaster socket; it died ten minutes after the user went idle. -i alone did not bypass it; ControlMaster=no did.
  • The host had no swap left (6 GB, 100% used, ZFS ARC at 5.9 GB), which correlates with five SSH drops in an evening.
  • A line typed into the coordinator's prompt while its scheduled tick runs lands in a state that Enter will not submit. Three user instructions sat unsent for up to forty minutes. Sending fresh text works; pressing Enter on a stale line does not.
  • Long pastes eat their trailing Enter; send it twice.

What I would do again

The role split by account. The sealed workers. Sabotage before trust. The ledger as the measurement rather than a byproduct. Escalation by rule. Foisting every piece of work onto the account with headroom and holding the runner to one line per tick.

What I would change

Set the swap and the SSH identity before the first message, not after the fifth drop. Put the claims file in place before the first dispatch. Write the nomenclature before the first contract; it was written after twelve and cost two renames. Decide the currency and the taxonomy on day zero; both were decided by the user in a sentence each once asked.

Structural decisions that made the driver removable

Recorded at 02:40 on day two, when the plan for a driverless day was written.

  1. Roles assigned by account rather than by machine. A full weekly limit on one side never stalled the other.
  2. The spec governs; every other file says it is the bug if it disagrees. Authority lived in a file the coordinator owns, so it could rewrite twelve contracts without asking.
  3. The ledger inside the spec, written as things bite: 101 entries in a day. Judgment auditable afterwards instead of supervised during.
  4. Sealed workers: one unit, one worktree, one contract. Cross-unit questions become text edits rather than meetings.
  5. The coordinator never builds; verdicts stand on its own output, calibrated by sabotage. A red gate is a return, not a fix, so unattended work cannot quietly change a delivery.
  6. A mechanical escalation rule: two returns, sonnet, stop.
  7. Everything durable in git: verdicts as notes, claims as a file, spokes by copy with a manifest, the status tick as a commit. The pane was only the live channel.
  8. The coordinator's own ten-minute clock, reporting whether or not anyone listens; on day two it polls a GitHub issue on the same tick.
  9. Decisions pre-made in one sentence each (currency, taxons, languages, model ladder, R10), so the same sentences can arrive as issue comments.

The gap the day exposed: lines typed at the pane were the least reliable channel, sitting unsent three times. The GitHub-issue inbox should have been the design from the start.

Related