§1 — Why this exists

Dispatches are silent by design: a headless worker opens with one short message and then works through tool calls for twenty minutes. Without a live view the lead cannot tell a deep worker from a stalled one, and a second orchestrator session cannot tell that a node is already being worked.

The upstream skillset solved this with a separate daemon on its own port. Reckon already runs one systemd-managed server on 127.0.0.1:8765 serving every mounted project, so a second daemon would be a regression. This plan folds the watcher into that server as a native surface and — more importantly — exposes in-flight state through the read tools agents already call.

§2 — Deliverables

Server routes. GET /crew across all mounted projects and GET /crew/<project> for one. Each response joins the live pointers with the owning repository's ledger: member identity and role from the ledger, phase and last activity derived by tailing the run's machine-readable log. Phase derivation is per-backend and reuses the terminal-event recognition already built for dispatch — done on a terminal event, working when the log was written recently, idle otherwise. Rows carry direct plan_href and sprint_href links so the view is navigable rather than merely informative. Tailing is bounded — read the tail of the log, never the whole file.

The watcher surface. A fifth glyph tab in the SPA shell beside Overview, Plans, Sprints and Graph, hash-routed consistently with the existing four, polling /crew. One row per run: member, role, plan and section with links, resolved model and effort, elapsed time, phase, last activity, and the gate the run is working toward. Follow the repository's minimal-ink figure discipline — this is a table of labels and values, so it is a semantic HTML table, never cards or tiles.

In-flight bands on plans and sprints. A plan page shows a band when any live run targets it; a sprint view badges each item that is in flight. This is what makes the live state answer the question the lead actually asks — not "what is the crew doing" but "is this being worked".

In-flight through the existing read tools. read_plan gains an in_flight block and roadmap rows gain an in_flight field carrying run id, member, section and start time. This is the cheapest available guard against double-dispatch: a second orchestrator, or a worker reading its own plan, sees a node is taken without needing to know the crew tool exists. For a node holding write scope a duplicate dispatch risks a conflicting second commit.

Repair the measured MCP and installation defects. Each was found while exercising the tools during design:

DefectRepair
roadmap(project="*") returned 176 KB and exceeded the response ceiling, making the portfolio scan unusable through MCP. read_plan and audit both take a progressive view; roadmap takes none. Add the same progressive view contract — summary default for a portfolio, with detail and raw opting deeper, plus pagination on findings.
~/.claude/skills/reckon-roadmap is a real directory where the other six reckon skills are symlinks. Contents are currently identical, so this is latent drift rather than active divergence. Make reckon install-skills detect and report a copied-where-linked skill, and repair this one.
agent-plan-authoring carries milestone: "—": an em-dash passes schema validation as a milestone identifier. Constrain the milestone field to an identifier shape or empty, and clear the placeholder.

§3 — Evidence gates

MeasureRequired evidence
Live run visible GET /crew reports phase=working within 3 s of a real dispatch, and done after the worker's terminal event. Both latencies recorded.
Visible to agents too While that run is live, read_plan on its plan and the matching roadmap row both carry in_flight with the run id. Recorded payload excerpts.
Navigable The watcher tab renders the run with working links to its plan and sprint, and the plan page shows its in-flight band. Screenshot banked as a figure.
No second daemon Only one listener on :8765 and no new port; confirmed by ss -ltnp output with the watcher in use.
Bounded reads A test proving log tailing reads a bounded tail rather than the whole file, on a log grown past that bound.
Portfolio scan usable roadmap(project="*") default view returns a response that fits well inside the MCP ceiling while still reporting per-project completion, ready and blocked counts and finding totals. Byte size recorded against the 176 KB baseline.
Drift detectable reckon install-skills reports the copied-where-linked case on a seeded fixture, and the live instance is repaired.
Placeholder rejected A test proving an em-dash milestone is refused on write; the live plan is cleared.
Suite green Full reckon suite passes; new-test count recorded.

§4 — Constraints

The server derives phase on read and stores nothing. Adding a background poller inside the server would create a second writer of run state and reintroduce the coupling the three-tier ownership split exists to avoid.

Server, parse and render changes are not hot. A restart is required and must be coordinated, since a running server and every connected MCP stdio process hold the old code in memory.

§5 — Recovery classification must read what the worker delivered

Run-state legibility is this plan's subject, and the classifier that a fresh orchestrator session depends on most is currently able to recommend promoting a run that delivered nothing. classify_pointer decides delivery from manifest_present or phase in ("complete", "failed"), and each half of that expression fails differently.

A terminal turn is not a terminal node. The stored phase reaches complete when the backend emits its end-of-turn event, which fires whenever the worker's turn ends — including a turn that ended in a NEEDS-HELP: surrender having produced nothing at all. Measured on this project: a run whose launch was rejected before its command started stored phase=complete with no manifest on disk, and recover classified it completed_unpromoted with the next action reckon crew complete --gate <verdict> --commit <sha> — a commit that does not exist, for work that never ran. Following that advice writes a fabricated success into the committed ledger, which is the one record later calibration treats as ground truth.

A failed run is advised to be promoted as a success. The same expression puts "failed" inside the terminal set, so the classification and the next action are identical for a run that failed and a run that succeeded.

Manifest presence is not manifest content. A blocked manifest exists exactly like a complete one, so a worker that correctly reported status: blocked is also classified as promotable. Delivery is a claim the manifest makes about itself, so the classifier must read the claim rather than infer it from the file existing.

The repair is to make the manifest's own status the authority on delivery, keep liveness where it already is in the process table, and let a run whose evidence disagrees with its phase report that disagreement rather than resolving it silently in favour of promotion.

MeasureRequired evidence
Empty delivery is not completion A pointer whose stored phase is terminal but whose manifest is absent classifies as abandoned, and its next action names the launch log rather than promotion. Asserted by test.
Blocked and failed are distinguished Manifests reading status: blocked and status: failed each classify separately from a complete one, and neither returns a promote action. Asserted by test.
Both readers agree The same pointer returns the same classification through the crew read tool and through the recovery command, since both already share one function. Asserted by test.
No fabricated promotion A test proving that no classification path emits a promote action for a run with no commit and no manifest.

§6 — Cleanup must consult live run state before removing a worktree

Worktree cleanup preflights for tracked and untracked changes and for reachability from the integrated branch, and refuses on either. Both checks protect committed or uncommitted work. Neither asks whether a worker is still running there.

So a node that has produced no commit yet — a read-only analysis, a verification run, anything early in its turn — presents as a clean worktree whose HEAD is already an ancestor of the primary branch, and cleanup removes it out from under a live process. Measured here: a session-wide cleanup removed five worktrees, one of which belonged to a run still in flight. The worker had read its ledger, was partway through its analysis, found its own working directory gone, and correctly refused to continue elsewhere rather than silently completing outside its fence. Its manifest reported the anomaly precisely; nothing was corrupted, and the node was redispatched. But the failure mode is a lost node with no signal beyond the worker noticing, and a worker that had not noticed would have written results from an unfenced location.

The live pointers already hold everything needed to prevent this: each names its worktree, and this plan's own work made that set readable in one call. Cleanup should consult it and refuse a path that a live run still claims, in the same visible-blocker style it already uses for a dirty tree. That is strictly better than asking the caller to remember, because the caller who forgets is an orchestrator mid-wave with several sessions of state in flight.

MeasureRequired evidence
A claimed worktree is refused Cleanup refuses to remove a worktree named by a live run pointer, reporting the run id, and removes it once that pointer is gone. Asserted by test.
The refusal is visible, not silent The refusal appears in the command's own result alongside any dirty-tree refusals rather than as a skipped path. Asserted by test.
A stale pointer does not block forever A pointer whose process is gone and whose classification is abandoned does not prevent cleanup, so a crashed run cannot strand its worktree permanently. Asserted by test.

§ Decisions

Where does the live watcher run?

Reckon already runs one systemd-managed, lingering-enabled server serving every mounted project, with plan and sprint context the watcher needs to link into. A second daemon would need its own supervision, its own port and its own enrolment step, and could not band a plan page with live state.

Is phase derived on read or maintained by a poller?

A poller would make the server a second writer of run state, reintroducing exactly the coupling the three-tier ownership split avoids. Deriving on read keeps a single writer and costs one bounded tail per request.

§ Followups

Fold the watcher into the server and expose in-flight to agents

Land the crew routes with read-derived phase, the fifth SPA tab, in-flight bands on plan and sprint views, in-flight blocks on the existing read tools, and the three measured MCP and installation repairs.

/reckon-ship inflight-visibility

All ten executable nodes landed. Crew routes with phase derived from a bounded 65,536-byte tail; the watcher tab as a fifth glyph route polling every 3s; in-flight bands on plan and sprint views; in_flight on read_plan and roadmap from one shared projection so both surfaces cannot disagree; the portfolio scan cut from 180,224 to 2,873 bytes with all 12 project rows intact; the milestone field constrained to an identifier or nothing while reads stay lenient; copied-where-linked skill detection with repair behind a flag; recovery classification grown from three classes to five so a blocked or failed manifest and a terminal turn that delivered nothing are no longer advised for promotion; and liveness-aware worktree cleanup, added after this session's own cleanup removed a running worker's tree. Registering the crew component also exposed three independent SPA entry points carrying their own script lists, now asserted identical by test. Four declared measures remain partial pending one service restart and are carried on this plan as f-ifv-002.

Discharge the live-instance measures once the server carries the new code

Four declared measures need a running instance of the merged code and cannot be produced from a checkout. Server, parse and render changes are not hot, so the HTTP server and every connected MCP process still hold the pre-merge code.

  • Live run visibleGET /crew reporting phase=working within 3 s of a real dispatch and done after the terminal event, both latencies recorded.
  • No second daemonss -ltnp output with the watcher in use, showing one listener on :8765 and no new port.
  • Navigable — the watcher tab rendering a live run with working plan and sprint links, banked as a figure.
  • In-flight band — a plan page showing its band while a run targets it, in the same figure or a second one.

The unblock is reckon service restart plus an MCP reconnect. It was not taken unilaterally because several concurrent agent sessions on this workstation read this server. The same restart also unblocks the sibling plan's direction adoption, so one restart discharges both.

Also outstanding and independent of the restart: the live reckon-roadmap skill directory is a real directory in both ~/.claude/skills and ~/.agents/skills where its siblings are symlinks. Detection and repair are landed and tested; the live repair needs install-skills --repair at a moment when no session is mid-read of those directories.

/reckon-ship inflight-visibility
crew-routes landed — commit 7c1c3de534ff22301a3630ffa0bc91a1272104c1; gate passed; measure: tests/test_crew_routes.py 7 passed / 0 failed in 3.4s with 7 new tests; a terminal turn.completed event classifies phase done, a log inside the 900s freshness window classifies working and an older one idle, and on an oversized stream exactly one 65,536-byte read was observed so the whole file is never loaded; aggregate and project-filtered routes both returned the fixture pointers and the route persists nothing. Two declared measures are only partly discharged and stay open: the 3-second live-dispatch latency needs a restarted server, and navigability needs the watcher surface. Artifacts reckon/serve.py, tests/test_crew_routes.py, docs/evidence/archive/inflight-visibility-landed.html#crew-routes
watcher-surface landed — commit 1cfe5cad344681688e3fe6e10958a0582b4dce29; gate passed; measure: tests/test_build.py 12 passed / 0 failed in 1.9s with 1 drift test and 4 new assertions, and reckon build exit 0 copying 17 UI and 5 shared files with _ui/crew.jsx emitted in its index; a fifth glyph tab hash-routes as #crew and survives project navigation, and crew.jsx polls GET /crew every 3s rendering one semantic tbody row per run over ten fields. The node first audited rather than wrote, and found three independent SPA entry points each carrying their own JSX script list — reckon/serve.py, reckon/cli.py and docs/index.html — none of which loaded the new component; tests/test_build.py now asserts the three normalised JSX sets are identical, so the hand audit became a standing check. Recorded scope-changed: those three files were granted mid-flight. RESTART OUTSTANDING — serve.py and cli.py changed and are not hot, so the route and tab exist in the repository and not yet in the live instance. Artifacts docs/evidence/archive/inflight-visibility-landed.html#watcher-surface
milestone-identifier-shape landed — commit 9b483972468d977de2ff8a96f1d7be63802449f3; gate passed; measure: tests/test_schema.py + tests/test_capability.py 1,102 passed / 0 failed with 4 new validation cases; strict write validation refuses milestone=— with an error naming the field while M2 and empty both remain accepted, the regenerated schema carries the constraint as ^(?:[A-Za-z0-9][A-Za-z0-9._-]*)?$ with an empty default, and the placeholder in docs/plans/agent-plan-authoring.html is cleared. Reads stay lenient by design so an existing document carrying the old placeholder remains readable rather than becoming unopenable the moment the constraint landed. Artifacts docs/evidence/archive/inflight-visibility-landed.html#milestone-shape
roadmap-progressive-view landed — commit 1d4543a511f277926028f948b54a8645cf156935; gate passed; measure: tests/test_mcp_roadmap.py 12 passed / 0 failed with 5 new test functions; project="*" defaults to view=summary and the live 12-project response measures 2,873 bytes against a 180,224-byte baseline, a 98.4% reduction and 1.6% of the figure that made the call unusable, inside the tested 32 KiB ceiling, while still reporting 12 projects, 361 plans, 177 completed, 86 ready, 50 blocked, 11 deferred and 82 findings with all 12 per-project rows retained. Explicit detail paginates findings by cursor, explicit raw wraps the legacy report, and a single-project call with no view keeps its existing shape. Artifacts docs/evidence/archive/inflight-visibility-landed.html#portfolio-view
skill-link-drift landed — commit fb95f4f41b864663661608d263bc0c82842c538a; gate passed; measure: tests/test_doctor.py 9 passed / 0 failed with 3 new tests, ruff clean; reckon install-skills infers the expected root from sibling symlinks, names the offending skill and the exact expected target, keeps repair behind an explicit --repair flag, and produces no finding for a runtime whose skills are consistently copied. Live inspection found TWO findings rather than the one anticipated: reckon-roadmap is a real directory in both ~/.claude/skills and ~/.agents/skills, while ~/.codex/skills is clean because all seven skills are consistently copied there. Both were left unmodified with identical mtime_ns before and after, because roughly a dozen concurrent agent sessions read those directories. The measure is recorded partial: detection is proven, the live repair is an outstanding coordinated action. Artifacts docs/evidence/archive/inflight-visibility-landed.html#skill-drift
inflight-in-read-tools landed — commit 2f1bb2335b21171a78675f2d2c1ddd164c2b0e3f; gate passed; measure: tests/test_mcp_views.py + tests/test_roadmap.py 53 passed / 0 failed in 1.0s with 6 new test functions; typed plan reads derive an optional in_flight list across summary, detail, history and raw views carrying run_id, member, section and started_at, and roadmap pending-work rows use the same shared projection so one pointer produces identical data on both surfaces rather than two implementations that can disagree. Unmatched plans, non-plan resources, archived plans and pointerless roadmap rows all omit the key; the path calls crew.list_live and neither persists nor mutates; the response schema advertises the projection; the legacy untyped shape is unchanged. This is the double-dispatch guard — a second orchestrator or a worker reading its own plan sees the node is taken without knowing the crew tool exists. Artifacts docs/evidence/archive/inflight-visibility-landed.html#inflight-read-tools
inflight-bands landed — commit 3f7c6d3983e84bc6c3a77c24f5a4c9c78dd81abc; gate passed; measure: tests/test_build.py 15 passed / 0 failed in 2.7s with 1 new test function and 9 assertions, reckon build exit 0; plan and sprint views poll the project-scoped crew route every 3s without persisting, a plan renders one accessible band listing each matching run's id, member and section, a sprint groups runs by plan slug and badges each matching card with a count and a title naming members and sections, and no match renders no band and no badge so the surfaces stay silent when nothing is in flight. The three-entry-point drift test stayed active and passed. All eight executable nodes of this plan have now landed; the remaining declared measures need the coordinated server restart and a banked screenshot, which are recorded as partial rather than passed. Artifacts docs/evidence/archive/inflight-visibility-landed.html#inflight-bands
recovery-classification landed — commit e2fb7886b10535ae66e93e15b01544d0e3288998; gate passed; measure: tests/test_crew.py 72 passed / 0 failed in 4.5s with 6 new tests, ruff clean; the vocabulary grew from three classes to five — running, completed_unpromoted, blocked, failed, abandoned — and status complete is now the only manifest status producing promote advice. A terminal stored phase with no manifest classifies abandoned with a next action naming stderr.log; blocked and failed manifests classify distinctly and never produce promotion advice; the MCP crew live view and CLI crew recover returned identical classification, manifest_status and next_action for the same pointer; and direct, MCP and recovery paths all classify a terminal run with no manifest and no commits as abandoned. This closes the hazard that produced the section: the old advice would have written a success with a nonexistent commit into the ledger that calibration reads as ground truth. Artifacts docs/evidence/archive/inflight-visibility-landed.html#recovery-classification
Found by causing it. A session-wide worktree cleanup removed five trees, one belonging to a run still in flight: cleanup preflights for dirty state and for reachability from the integrated branch, and a node that has produced no commit yet passes both, so a live read-only analysis presents as a clean removable tree. The worker read its ledger at c840cdb with 75 records, was partway through its analysis, found its working directory gone, and correctly refused to continue outside its fence rather than silently completing elsewhere — its manifest reported the anomaly precisely and the node was redispatched under a separate session id. Nothing was corrupted, but a worker that had not noticed would have written results from an unfenced location. The live pointers already name their worktrees and this plan made that set readable in one call, so cleanup should consult it and refuse a claimed path in the same visible-blocker style it uses for a dirty tree. Recorded as §6 with three measures including one that stops a stale pointer stranding a worktree forever. Denominator grew from 9 nodes to 10, so impl reads 9/10.
cleanup-consults-live-runs landed — commit e96cac111d56e57d0c592a9397a4bbb3138cc083; gate passed; measure: tests/test_worktree_fleet.py + tests/test_crew.py 82 passed / 0 failed in 7.4s with 3 new tests; a worktree whose run classifies as running is refused with the claiming run id reported under claimed_by_live_runs, active-claim and dirty-tree refusals are returned together rather than one masking the other, deleting the pointer permits removal of the same worktree, and a dead-pid pointer classifies as abandoned and does not block removal because liveness is re-read from the process table rather than trusted from the stored phase. That last property keeps the new check from becoming its own trap: a gate trusting the stored phase would let any crashed run hold its worktree forever, trading a rare destructive error for a permanent leak. All ten executable nodes of this plan have landed. Artifacts docs/evidence/archive/inflight-visibility-landed.html#cleanup-liveness
cleanup-without-package-import landed — commit d59528092057da5cd1642d98bdced3938be807d1; gate passed; measure: tests/test_worktree_fleet.py + tests/test_crew.py 85 passed / 0 failed in 7.8s with 3 further new tests, ruff clean. The first liveness implementation imported reckon.crew, which broke the standalone script the orchestration reference invokes as bare python in two places — the next cleanup call failed with ModuleNotFoundError, and the node's own gate could not see it because pytest runs inside the project environment. The script now reads /crew/live/*.json directly and checks PIDs against the OS with zero reckon imports: under an interpreter hook rejecting every reckon import, create exited 0, cleanup-session exited 0, a live pointer still produced the visible claimed-worktree refusal with exit 2, and deleting the pointer let cleanup exit 0. An AST inspection asserts the import's absence so the regression cannot return quietly. Two alternatives were rejected: editing the docs to match the regression, and wrapping the import in a try that skips the check — a liveness guard that silently does nothing when invoked the documented way is worse than the defect it fixed. Verified by running the documented bare-python invocation, which removed its own worktree. Artifacts docs/evidence/archive/inflight-visibility-landed.html#cleanup-liveness