Crew run ledger — landed evidence
The durable half of a run now lives where the plan it implemented
lives: <repo>/docs/state/<project>/crew.json, committed beside
index.json and version-paired the same way. The transient half never leaves
reckon's config home. Eight declared measures produced evidence, 28 new tests cover them, and
the suite is green at 1,628. One measure is reported as mechanism-proven but data-empty:
reckon's own ledger begins now, so it has no measured spread to show yet.
Declared measures and what each produced
| Measure | Evidence | Verdict |
|---|---|---|
| Round-trip | A real CLI round-trip: pointer written at dispatch, ledger at
version 2 after promotion, pointer gone, and git status showing
crew.json as the only changed state file — § round-trip | pass |
| Nothing transient is committed | No pointer, log or crew-home path resolves
inside the working tree, and an in-flight run leaves git status --porcelain
empty — § transient | pass |
| Interruption is recoverable | A killed run that delivered classifies as completed-but-unpromoted with its manifest path, and promotion then succeeds; all three classes exercised in one report — § recovery | pass |
| Concurrent writes are safe | Two interleaved promotions both survive, the retry provably forced; a stale version is refused and a double promotion is refused — § concurrency | pass |
| Session reuse | A null-session member captures an id on its first run; a
second node's argv carries resume <that id> — § sessions | pass |
| Worktree routing | Against a real git worktree,
crew(…, checkout_path=…) reads that checkout's ledger while the main
checkout's read returns its own — § routing | pass |
| Calibration inputs captured | All 20 record fields present on a promoted
run; scope_changed defaults false and is settable; the report shows 40.0 min
over 2 runs against declared effort M with a 20.0 min spread, and excludes the
scope-changed run — § calibration | pass — data-empty for reckon itself, see below |
| Suite green | 1,628 tests pass, 28 of them new; lint clean — § suite | pass |
The shape that makes recovery possible
Promotion is two writes in a fixed order, and the order is the whole recovery story. The ledger is appended first and the pointer deleted second, so the only state an interruption can leave is a pointer whose run has finished — which is recoverable. The reverse order would lose the record outright.
Round-trip, run through the CLI
Recorded against a throwaway repository with the shipped in-harness backend, so nothing was spawned. The roster write created the ledger at version 1; the promotion appended to it at version 2.
| Live pointers | docs/state/smoke/ | git status --porcelain | |
|---|---|---|---|
| after dispatch | r-20260812T093244038067-node-a.json | crew.json, index.json | ?? docs/state/smoke/crew.json |
after crew complete | none | crew.json, index.json | ?? docs/state/smoke/crew.json |
The ledger is the only state file either step touched. It is untracked in the listing above
only because the throwaway repository never committed it; in a real project it is committed
beside index.json, which is the point of putting it there.
The promoted record, abridged to the fields the measures name:
run_id r-20260812T093244038067-node-a plan/section plan-a / §2 node node-a member worker-a agent backend native, launch in-harness, sandbox worktree-full dispatched 2026-08-12T09:32:44Z completed 2026-08-12T09:32:44Z base_sha c57aeb3f116a3a5680edc04842320035e2888e6c commits b2f9f7d changed_lines added 1, removed 0, files 1 tests_added 28 gate passed scope_changed false time_budget 20m
A gate verdict is one of passed, failed or
not-run, refused otherwise. Free text would have made the field unqueryable by the
calibration loops that will read it, and not-run is a real answer: a gate whose
evidence could not be produced is a recorded negative, not a silent pass.
Nothing transient reaches a working tree
Two tests hold the boundary from both sides. The first asserts that the crew home, the live
directory, the pointer file and the event log are all outside the repository — the property that
would otherwise decay the first time someone moved the config home. The second asserts that a
run in flight leaves git status --porcelain empty in the main tree, with the
worker's own scoped files confined to its detached worktree, which is itself outside the
repository.
An interruption is recoverable, and recovery is conservative
A run whose process was killed after it delivered its manifest classifies as
completed_unpromoted, reports the manifest path, and names the exact promotion
command; promoting it then succeeds, so nothing was lost by the interruption. A run whose process
died having delivered nothing classifies as abandoned and points at its stderr log.
A live process classifies as running. One report over three concurrent runs returned
exactly one of each.
Recovery repairs the record and nothing else: after a recover pass over an abandoned run, the worktree is still there and the pointer is still there. Removing either would destroy the evidence a human needs to decide, which is why the conservative posture already in the fleet script is matched here rather than reinvented.
Two orchestrators cannot clobber each other
Ledger writes are paired with the version they read, exactly as index.json
writes are, and a stale version is refused with an instruction to re-read. Appending retries on
that refusal, which is what makes interleaving safe rather than merely detected: with a competing
record injected mid-write, the loser re-read the winner's ledger and appended to it, leaving all
three records — and the test asserts the retry actually fired rather than inferring it from the
result.
A second record for the same run id is refused instead of retried. That is not concurrency, it is a double promotion, and it would double-count every measurement the record carries.
A member's session outlives its first node
A member registered with no session captures one from its first run: observation is where a
backend's session id first becomes knowable, so that is where the roster learns it — waiting for
completion would leave a second node dispatched in the meantime unable to reach the same session.
A second node dispatched to that member launched with resume <the captured id>
in its argv.
The first id wins. A later run reporting a different session is reported and not written over the top, because overwriting would silently retire the long-lived session every subsequent node and every escape-hatch resumption is meant to reach. Dispatching to an unregistered member is refused before any worktree exists, naming the command that registers one.
A worker inside a worktree reads its own ledger
Verified against a real second checkout cut with git worktree add, holding a
different record from the main one. crew(project, view="ledger",
checkout_path=<worktree>) returned the worktree's record and a path under the
worktree; the same call against the main checkout returned the main record. The contract is the
one read_plan already uses, so nothing new has to be learned to use it.
What makes effort falsifiable later
Effort is an asserted letter with no external referent. The ledger is what can make it
falsifiable, but only if the fields are captured while they are still knowable — after the
worktree is gone, the wall-clock, the configuration that ran the node and the scoped diff cannot
be reconstructed. All twenty record fields are present on a promoted run, including the
agent block and a changed_lines count taken from the diff restricted to
the node's own write paths: a count over the whole diff would describe the branch, not the
node.
scope_changed defaults false and is settable. It is the field easiest to omit and
impossible to reconstruct, so the report excludes those runs from every measured column and
counts them separately rather than averaging them in — a scope-changed run measures neither the
estimate nor the worker. On recorded data, two runs of a plan declared effort M reported measured
40.0 min, mean 20.0, min 10.0, max 30.0, spread 20.0; adding a 100-minute scope-changed run left
the mean at 10.0 with one run excluded.
Stated rather than implied: reckon's own ledger begins with this change, and
this plan was implemented inline rather than through crew dispatch, so the project
has no crew-dispatched runs yet and its measured spread is empty. The mechanism is proven on
recorded data; the first real spread appears once nodes are dispatched and promoted through the
CLI. Declared effort is read from the plans themselves, so the claim and the measurement cannot
drift apart. Weighting and the unit migration remain effort-calibration's.
Suite, lint and the tool surface
uv run pytest -q → 1,628 passed, up from 1,600, with 28 new
tests in tests/test_ledger.py. uv run ruff check reckon tests → all
checks passed. The MCP surface is asserted equal to exactly five tools —
read_plan, edit_plan, roadmap, audit,
crew — so a sixth cannot be added without the assertion failing.
One defect surfaced and was fixed during the work: the ledger reached the store's classes by importing their names, and a test that reloads the store module rebinds those classes in place, so a captured exception class no longer matched the one its own function raised. Two tests passed in isolation and failed in the suite. The module now reaches the store through the module object, which is why the whole-suite run is the one that counts.