§1 — Why this exists

Plans are repo-local and committed, so the record of how a plan was implemented should be too. But a run has two natures that must not share a home: while a worker is in flight its record changes every few seconds and is worthless once the run ends; once complete, the record is durable evidence. Storing either in plan HTML is wrong — every progress tick would bump the server-owned version counter and fight optimistic concurrency.

The three-tier split, and why ~/.config/reckon/state/<project> being a symlink into the repository gives the durable half its properties for free, are recorded in Agent Fleet Orchestration §5.

✓ landed 2026-08-12

§2 — Deliverables

Built reckon/ledger.py plus promotion, three-way recovery, roster-backed session reuse and the read-only crew MCP tool. A run's durable record now lands in <repo>/docs/state/<project>/crew.json, committed beside index.json; the transient pointer never leaves the config home. 28 new tests, suite green at 1,628; the MCP surface is asserted at five tools. Full record: §2 landed (commit 3bf7865).

✓ landed 2026-08-12

§3 — Evidence gates

All eight declared measures produced evidence and passed: round-trip (ledger v1→v2, pointer gone, crew.json the only changed state file), nothing transient committed, all three recovery classes, interleaved writes both surviving, session capture and resume, worktree routing against a real worktree, all 20 calibration fields with the scope-changed exclusion, and 1,628 tests green. One measure is mechanism-proven but data-empty: reckon's own ledger starts here, so it has no measured effort spread yet. Full table with per-measure evidence: §3 landed.

§ Evidence gates

Round-trip

A dispatch writes a pointer; completion promotes it into the repository ledger and removes the pointer. Recorded before-and-after file listings plus the resulting git status showing the ledger as the only changed state file.

Evidence

Nothing transient is committed

A test asserting no live-pointer path resolves inside any repository working tree, and that an in-flight run leaves the working tree clean apart from the worker's own scoped files.

Evidence

Interruption is recoverable

A killed run leaves a pointer that recover classifies as completed-but-unpromoted, reporting the manifest path; promotion then succeeds. All three classifications exercised by test.

Evidence

Concurrent writes are safe

A test proving two interleaved ledger writes both survive — version-paired, no lost update.

Evidence

Session reuse

A member registered with a null session captures an id on first run, and a second node against that member demonstrably resumes the same session.

Evidence

Worktree routing

crew(..., checkout_path=…) reads the worktree's ledger, not the registered main checkout's. Verified against a real worktree.

Evidence

Calibration inputs captured

A completed run record carrying dispatch and completion timestamps, agent configuration, changed lines, tests added, gate outcome, and the scope-changed flag, with a test proving that flag is settable and defaults false. Measured worker-time per completed plan reported against its declared effort with the observed spread stated. The mechanism is proven on recorded data; reckon's own ledger is data-empty until nodes are dispatched through the CLI.

Evidence

Suite green

Full reckon suite passes; new-test count recorded.

Evidence

§ Decisions

Where does run state live?

Plans are repo-local and committed; their implementation record arguably should be too. But in-flight state churns too fast to commit.

A repository owns every durable fact about its own implementation, exactly as it owns its plans; reckon holds only the pointer that lets one view aggregate across mounted projects. Plan HTML was rejected because every progress tick would bump the version counter and fight optimistic concurrency.

§ Followups

Build the run ledger, pointers, promotion and recovery

Land the committed per-project ledger, ephemeral live pointers, version-paired promotion, three-way orphan classification, roster-backed session reuse, and the read-only crew MCP tool with worktree routing.

/reckon-ship crew-run-ledger

Landed — commit 3bf7865. Ledger at docs/state/<project>/crew.json with version-paired writes, promotion before pointer deletion, three-way recovery, roster session capture and reuse, all 20 calibration fields with the scope-changed exclusion, and the read-only crew MCP tool holding the surface at five tools. 28 new tests; suite 1,628 green. All eight evidence gates passed; the effort report is mechanism-proven but data-empty for reckon until nodes are dispatched through the CLI.

Dispatch the work the ledger unblocked

The ledger and its roster are in place, so the three plans that depended on it are now executable. budget-aware-dispatch is the nearest: it reads the same run records and the budget signal already captured on every pointer. inflight-visibility and effort-calibration follow, the latter owning the unit migration this plan deliberately left alone.

First real measurement note: reckon's own ledger is empty until nodes are dispatched and promoted through reckon crew, so the effort spread stays unreported until then.

/reckon-ship budget-aware-dispatch

Make post-completion continuity structural, not dependent on remembering a flag

A followup on landed work should reach the worker that did it, but promotion deletes the live pointer and its session_id with it. The session survives only where capture_session recorded it — the roster entry in committed crew.json — so a node dispatched without --member can never be continued after it lands, and the loss is silent at the moment it matters.

The reckon-ship skill now states the routing rule (a needs-help brief resumes its live run, a followup within a landed node's scope returns to its roster member, new scope is a new node) and tells orchestrators to dispatch as a member by default. That binds discipline, not the tool. Close the gap in code: either have dispatch attach a default roster member when none is named, or have crew complete retain the run's session against its member before deleting the pointer. Measure it with a test that completes a run and then resumes its session.

/reckon-ship crew-run-ledger

Hold or divert a dispatch onto a busy member instead of letting it die at launch

A roster member's session is a single-writer resource, so reusing a member whose previous run still holds the writer lock kills the new run at launch: thread-store conflict: thread <id> already has an active writer, exit before the first event. Observed here dispatching a node to a member twenty seconds after that member's prior run finished; two sibling nodes on free members launched normally in the same wave.

The failure is worse than its cause because it is silent. The run directory holds a zero-length stream.jsonl, no manifest is ever written, and observe reports orphaned - the classification meaning "finished without a terminal event", which is indistinguishable from a mid-work crash. The actual reason sits in stderr.log, which nothing reads or surfaces. An orchestrator following the recovery ladder checks the manifest, then on-disk evidence, then re-asks the worker, and finds nothing at every step, because the worker never existed.

Two repairs. Make dispatch detect a held session before launching and either wait for release, or fall back to a fresh session with that substitution reported - continuity is worth less than the node. And make observe read stderr.log when the stream is empty, so a launch failure classifies as launch-failed with its reason attached rather than as an orphan. The skill now carries the operational rule (a wave needs as many members as concurrent nodes; read stderr on a zero-length stream), but a rule that a tool could enforce should not stay a rule.

/reckon-ship crew-run-ledger

Make the live view's phase as fresh as the rest of its payload

The crew tool's live view recomputes process_alive, manifest_present, log_age_seconds and log_fresh on every read, but takes phase from the stored pointer. Phase is the field a reader actually wants, and it is the one that lags: measured here, a run 41 seconds into a 77KB event stream reported starting, and only flipped to working after a CLI observe wrote the record.

That contradicts the tool's own contract. It is read-only by design, yet its headline field only becomes accurate after a mutation the caller must know to perform - and the correction requires the CLI, so a pure MCP reader can never get a true phase. The tool already reads the log's mtime for log_age_seconds, so the stream is in hand; fold the phase from it on read and leave the write to observe.

The same read is where token usage is captured, so the two defects share a remedy: whatever folds the stream for the live view should also be what complete calls before promoting.

Two smaller findings from the same exercise. The tool's docstring advertises four views while the code accepts five - a budget view exists in reckon/mcp.py that the description never mentions, and a stdio server started before it landed rejects the view outright, which is the documented cold-code behaviour rather than a bug. And the description says what the tool reads without saying when to reach for it: it should name the situation - checking on background workers without interrupting them - and name the wrong alternative it replaces, because an agent that does not see itself in the description will shell out to the CLI instead. That is exactly what happened before this was written.

/reckon-ship crew-run-ledger

Classify a run from what its manifest says, not from the fact that a file exists

A worker that gives up is reported as a worker that succeeded. classify tests manifest.is_file() and never opens it, so status: blocked, status: failed and status: complete all resolve to completed_unpromoted with the same detail - "the run finished and its record is still a pointer" - and the same next_action: reckon crew complete --gate <verdict> --commit <sha>. Measured here on gate-schema-retry, which stopped after two failed patch attempts having changed nothing: the live view advised promoting it, for a run with no commit, no changed paths and no tests.

The capability to do better already exists and is simply unwired. parse_manifest extracts status, and parse_needs_help turns a NEEDS-HELP: report into its four structured fields. Both are called only from the writeback helpers; neither observe nor classify reads a manifest's contents at all.

The consequence is that no amount of diligence recovers the signal. An orchestrator polling the live view on a tight loop is told the node is complete and ready to promote. The blocked node in this session was found only because a human asked for a status update - roughly ten minutes after its manifest landed - and the recovery ladder's whole premise, that on-disk evidence tells you what happened, fails when the reader stops at the filename.

Classify from the manifest: blocked and failed become their own classifications carrying the worker's stated blocker, a manifest containing a needs-help brief classifies as awaiting advice with reckon crew resume --run <id> --advice as its next action, and only status: complete earns the promote advice. Then a blocked run cannot be promoted with a fabricated verdict, because the classifier already knows better.

/reckon-ship crew-run-ledger

Give a finishing worker a way to reach its orchestrator instead of waiting to be asked

Run state is pull-only. A worker that finishes, blocks, or dies changes a file and nothing else happens; the orchestrator learns of it exactly when it next thinks to look. Every visibility defect recorded against this plan shares that root: the information was on disk and correct, and no one was told. A blocked node sat unnoticed for ten minutes in this session, and a launch that died in one second was discovered fifteen minutes later.

The gap is not that polling is unimplemented but that the design has no push edge at all, while the surrounding harness does. A dispatched CLI worker is a process the orchestrator could be notified about on exit; the manifest is a file whose creation is watchable. Neither is wired, so every orchestrator invents its own waiting loop, and those loops test file existence - which cannot distinguish a delivered success from a delivered surrender, and cannot notice at all when the run it waits on died before writing anything.

Give dispatch an optional completion signal the orchestrator can wait on, carrying the finishing classification rather than the bare fact of exit, so waiting costs nothing and a blocked or launch-failed run announces itself. Pair it with the manifest-aware classification in the sibling followup: a push edge that reports "completed" for a surrender only moves the wrong answer faster.

/reckon-ship crew-run-ledger