In-flight visibility — landed evidence
One cumulative record, updated as each node lands. Anchors are stable; the plan's evergreen sections link to them as they collapse.
Declared measures and what each has produced
| Measure | Evidence | Verdict |
|---|---|---|
| Bounded reads | Exactly one 65,536-byte read observed on an oversized stream — § crew routes | pass |
| Live run visible | Phase classification proven on fixtures; the 3-second latency measure against a real dispatch is still open — § crew routes | partial |
| Navigable | The crew tab renders each run with linked plan and section; the screenshot figure and the in-flight band remain open — § watcher surface | partial |
| Visible to agents too | One pointer produces identical in_flight data on both the plan read and the roadmap row — § in-flight read tools | pass |
| No second daemon | The surface is a tab in the existing SPA on the existing port; no new listener was added — § watcher surface | partial |
| Portfolio scan usable | 2,873 bytes against a 180,224-byte baseline — a 98.4% reduction with all 12 rows intact — § portfolio view | pass |
| Drift detectable | Reported on a fixture and repaired behind a flag; two live findings reported without modification — § skill drift | partial |
| Placeholder rejected | An em-dash milestone is refused on write and the live plan is cleared — § milestone shape | pass |
| Suite green | — | open |
Crew routes, with phase derived on read
Commit 7c1c3de — reckon/serve.py,
tests/test_crew_routes.py; 419 insertions, 2 deletions across 2 files. Gate
passed: 7 focused tests, 0 failures, 3.4 s.
GET /crew and GET /crew/reckon each emit one row per mounted live
pointer, joining the pointer with the owning repository's ledger for member and role, and adding
plan and section, resolved model and effort, elapsed seconds, derived phase, last activity, the
gate the run works toward, and direct plan_href and sprint_href links.
The route reads live pointers, ledgers, plan discovery and bounded log tails, and persists
nothing — so the server remains a reader of run state rather than a second writer of it.
| Measure | Result |
|---|---|
| Phase from a terminal event | A stream ending in turn.completed classifies done |
| Phase from freshness | A log written inside the 900-second window classifies working; older classifies idle |
| Read is bounded | On a stream grown past the bound, exactly one 65,536-byte read was observed — the whole file is never loaded |
| Both routes | Aggregate and project-filtered routes each returned the fixture pointers |
| Scoped suite | tests/test_crew_routes.py — 7 passed, 0 failed; 7 new tests |
Two of this plan's declared measures are only partly discharged by this node and stay open: the 3-second latency figure needs a real dispatch observed through a restarted server, and the navigability measure needs the rendered surface that the watcher-tab node supplies. Both are recorded as partial rather than passed.
Cleanup now refuses a worktree a live run still claims
Commit e96cac1 — skills/reckon-ship/scripts/worktree_fleet.py,
tests/test_worktree_fleet.py; 139 insertions, 4 deletions across 2 files. Gate
passed: 82 passed, 0 failed, 7.4 s, 3 new tests.
This node exists because the orchestrator caused the defect during this sprint: a session-wide cleanup removed the worktree of a running analysis node. Cleanup preflighted for a dirty tree and for reachability from the integrated branch, and a node that had not committed yet passed both — so a live read-only worker looked perfectly removable.
| Behaviour | Result |
|---|---|
| An active claim refuses | A worktree whose run classifies as running is refused, and the result reports the claiming run id under claimed_by_live_runs |
| Refusals travel together | Active-claim and dirty-tree refusals are returned in the same result rather than one masking the other |
| Release permits removal | Deleting the pointer allows the same worktree to be removed |
| A crash cannot strand a tree | A dead-pid pointer classifies as abandoned and does not block removal; liveness is refreshed rather than trusted from the stored phase |
The last row is what keeps the new check from becoming its own trap. A liveness gate that trusted the stored phase would let any crashed run hold its worktree forever, trading a rare destructive error for a permanent leak — so the check re-reads the process table rather than the record.
The check broke the tool it protects, and then did not
The first implementation reached for reckon.crew to read live pointers. That script
is deliberately standalone — the orchestration reference invokes it as
python skills/reckon-ship/scripts/worktree_fleet.py … in two places — so the very next
cleanup call failed with ModuleNotFoundError: No module named 'reckon'. Its own gate
could not see this: pytest runs inside the project environment, where the import succeeds.
Commit d595280 — 193 insertions, 10 deletions across the same 2 files; 85 passed,
0 failed, 3 further new tests, ruff clean. The script now reads
<config-home>/crew/live/*.json directly and checks PIDs against the OS, importing
no reckon module at all. Under an interpreter hook that rejects 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 that
pointer let cleanup exit 0. An AST inspection asserts the absence of the import, so the regression
cannot return quietly.
Two choices are worth stating because the tempting alternatives were both worse. The docs were
not edited to match the regression — the script is documented correctly and was wrong. And the
import was not wrapped in a try that skips the check when unavailable: a liveness guard
that silently does nothing precisely when invoked the documented way would be worse than the defect
it was written to fix.
A plan and a sprint show which of their own work is in flight
Commit 3f7c6d3 — docs/ui/plan.jsx, docs/ui/sprint.jsx,
tests/test_build.py; 139 insertions, 16 deletions across 3 files. Gate
passed: 15 passed, 0 failed, 2.7 s, 1 new test function with 9 assertions;
reckon build exit 0.
Both views poll the project-scoped crew route every 3 seconds and persist nothing. A plan filters the response to runs whose plan equals its slug and renders one accessible band listing each run id, member and section; a sprint groups runs by plan slug and badges each matching card with an accessible label counting the runs and a title naming their members and sections. No match renders no band and no badge — the surfaces are silent when nothing is in flight rather than showing an empty container.
This is the question the lead actually asks. Not "what is the crew doing", which the watcher tab answers, but "is this being worked" — asked while already looking at the plan. The three-entry-point drift test stayed active and passed, so the new components are registered consistently across the live renderer, the static build and the checked-in index.
The tools agents already call now say a node is taken
Commit 2f1bb23 — reckon/mcp_views.py,
reckon/roadmap.py, tests/test_mcp_views.py; 233 insertions, 20 deletions
across 3 files. Gate passed: 53 passed, 0 failed, 1.0 s, 6 new test functions.
Typed plan reads derive an optional in_flight list from live pointers across the
summary, detail, history and raw views, each record carrying run_id,
member, section and started_at. 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.
| Property | Result |
|---|---|
| Both surfaces agree | One pointer, identical data on the plan read and the roadmap row |
| Silent when idle | Unmatched plans, non-plan resources, archived plans and roadmap rows without pointers all omit the key entirely |
| Read-only | The path calls crew.list_live and neither persists nor mutates state |
| Discoverable | The response schema advertises the optional projection |
| Non-breaking | The legacy untyped read shape is unchanged |
This is the cheapest available guard against double-dispatch: a second orchestrator — or a worker reading its own plan — sees the 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, which is the concrete failure this closes.
The portfolio scan fits, at 1.6% of its former size
Commit 1d4543a — reckon/mcp.py, reckon/mcp_views.py,
tests/test_mcp_roadmap.py; 199 insertions, 9 deletions across 3 files. Gate
passed: 12 passed, 0 failed, 5 new test functions.
project="*" now defaults to view=summary. Measured against the live
12-project mount set, the default response is 2,873 bytes against a 180,224-byte
baseline — a 98.4% reduction, and 1.6% of the figure that made this call unusable through
MCP — while comfortably inside the tested 32 KiB ceiling.
The reduction keeps what a portfolio scan is for. The payload still reports 12 mounted projects,
361 plans, 177 completed, 86 ready, 50 blocked, 11 deferred and 82 findings, with per-project
completion, ready, blocked, deferred and finding totals — all 12 rows retained rather than
truncated. Explicit detail gives cursor-paginated findings for portfolio and
single-project calls, explicit raw wraps the lossless legacy report, and a
single-project call with no view keeps its existing shape, so no caller changes behaviour without
asking to.
A copied skill directory standing among symlinks is now reported
Commit fb95f4f — reckon/cli.py, tests/test_doctor.py;
122 insertions, 5 deletions across 2 files. Gate passed: 9 passed, 0 failed,
3 new tests, ruff clean.
reckon install-skills infers the expected root from sibling symlinks and reports a
real directory standing among them, naming both the offending skill and the exact expected target.
Repair is behind an explicit --repair flag; the default path reports and changes
nothing. A runtime whose skills are consistently copied produces no finding, which is what
keeps the check from firing on a deliberately different installation style.
| Runtime | Finding |
|---|---|
~/.claude/skills | reckon-roadmap copied where linked |
~/.agents/skills | reckon-roadmap copied where linked |
~/.codex/skills | None — all seven skills consistently copied |
The live count is two, not the one the plan anticipated: the same skill is a
real directory in two separate runtimes. Both were left untouched, with identical
mtime_ns before and after inspection — roughly a dozen concurrent agent sessions read
those directories, and converting one to a symlink underneath a running session is a change to take
when the fleet is quiet. The measure is therefore recorded partial: detection is
proven, the live repair is an outstanding coordinated action.
Recovery reads the manifest instead of inferring delivery from a turn ending
Commit e2fb788 — reckon/crew.py, tests/test_crew.py;
193 insertions, 14 deletions across 2 files. Gate passed: 72 passed, 0 failed,
4.5 s, 6 new tests, ruff clean.
The vocabulary grew from three classes to five — running,
completed_unpromoted, blocked, failed,
abandoned — because three could not express the states that were actually occurring.
status: complete is now the only manifest status that produces promote advice.
| Case | Result |
|---|---|
| Terminal phase, no manifest | Classifies abandoned, and the next action names stderr.log — the one file a failed launch writes its reason to |
| Manifest says blocked | Classifies blocked; no promote advice |
| Manifest says failed | Classifies failed; no promote advice |
| Manifest says complete | The only status producing crew complete advice, with the manifest's own commits reflected in it |
| Both readers agree | The MCP crew live view and CLI crew recover returned identical classification, manifest_status and next_action for the same pointer |
| No fabricated promotion | Direct, MCP and recovery paths all classify a terminal run with no manifest and no commits as abandoned |
The last row closes the specific hazard that produced this section: an orchestrator following the old advice would have written a success with a nonexistent commit into the committed ledger, which later calibration reads as ground truth. The defect was found by hitting it, on the run whose launch the read-only sandbox rejected.
A milestone is an identifier or nothing
Commit 9b48397 — reckon/_schema.py,
docs/_shared/plan.schema.json, tests/test_schema.py,
docs/plans/agent-plan-authoring.html; 54 insertions, 4 deletions across 4 files.
Gate passed: 1,102 passed, 0 failed, 4 new validation cases.
Strict write validation refuses milestone="—" with an error naming the field,
while "M2" and "" both remain accepted. The published schema carries the
matching constraint as ^(?:[A-Za-z0-9][A-Za-z0-9._-]*)?$ with an empty default, and
the em-dash that had been sitting in agent-plan-authoring is cleared.
The split between strict and lenient is the part worth keeping: writes refuse the placeholder, but reads stay lenient, so an existing document carrying one is still readable rather than becoming unopenable the moment the constraint lands. A validation tightening that made old documents unreadable would be a worse defect than the one it fixed.
The watcher is a tab in the existing SPA, and three entry points now agree
Commit 1cfe5ca — docs/ui/crew.jsx, docs/ui/shell.jsx,
docs/ui/glyphs.jsx, docs/index.html, reckon/serve.py,
reckon/cli.py, tests/test_build.py; 185 insertions, 2 deletions across
7 files. Gate passed: 12 passed, 0 failed, 1.9 s, and
reckon build exit 0. Scope was widened mid-flight, so this run is excluded from
calibration.
A fifth glyph tab hash-routes as #crew consistently with the existing four and
survives project navigation. crew.jsx polls GET /crew every 3 seconds
and renders one tbody row per run over ten fields — member, role, linked plan,
linked section, model, effort, elapsed time, phase, last activity and gate — as a semantic table,
because rows of labels and values are a table and not a card grid.
The node's most useful output was an audit it produced before writing anything.
A component cannot render if no entry point loads it, and this SPA has three independent entry
points that each carry their own JSX script list: the live renderer in
reckon/serve.py, the static-build renderer in reckon/cli.py, and the
checked-in docs/index.html. All three loaded graph.jsx immediately
before shell.jsx, and none mentioned crew.jsx. Adding one component
therefore means editing three files that must agree, with nothing enforcing agreement — the same
latent-drift shape as the copied-where-linked skill this plan already carries.
So the repair went beyond registration: tests/test_build.py now asserts that the
normalised JSX sets loaded by the live renderer, the static-build renderer and the checked-in
index are identical. The hand audit that found this becomes a standing check, and a
component registered in two of three places now fails a test instead of silently failing to
render. The static build copied 17 UI files and 5 shared files and emitted
_ui/crew.jsx in its index.
Two measures move to partial rather than pass. No new listener or port was introduced, which
is most of the no-second-daemon claim, but the declared evidence is ss -ltnp output
with the watcher in use and that needs the restart below. Navigability has its links but not yet
its banked screenshot or its in-flight band.
A restart is outstanding. reckon/serve.py and
reckon/cli.py both changed, and server, parse and render code is not hot: the
running server and every connected MCP stdio process hold the old code until restarted. Until
that is coordinated, the crew route and the tab exist in the repository and not in the live
instance.