Derived gate state — landed evidence

A gate is a declared blocker, so making gates first-class and repairing the rotted blocked projection turned out to be one job rather than two. Every code rung has landed across five waves; the document rungs — the skill's own fence, this sprint set's prose gates, and the portfolio sweep — are recorded here as they close. The most useful single result is a failure: the wave-four suite gate went red and exposed a readiness collapse that no unit test had caught.

Declared measures and what each produced

MeasureEvidenceVerdict
Two-way derivationeffective_status('blocked', []) returns active where it previously stayed blocked forever, while a live blocker still yields blocked; the one-way behaviour is the seeded regression case — § derivationpass
Gate blocks and unblocksOne bidirectional test flips only the verdict from failed to passed and proves both read paths plus ready_now change in both directions, with persisted workflow status active throughout — § mechanismpass
Evidence-free pass rejectedA pass op with no recorded measure is refused while gates.require_evidence holds, and the refusal names the gate — § evidencepass
Blocked unwritableA write persisting blocked is rejected naming the status, a persisted sprint-item status is rejected on write, and legacy documents carrying either still read under a compatibility warning — § unwritablepass
Round-trip parityGates survive the state round-trip with byte-identity of the regenerated reckon-owned section, and passed is re-derived from verdict even when contradictory input supplies passed=true§ round-trippass
Concurrent sprints expressibleTwo sprints hold incomplete items simultaneously with no error raised; the uniqueness invariant is gone and the default focus derives from sprint order and incompleteness, so nothing persists it to go stale — § concurrencypass
Runnability deriveddispatchable and authorised are separate derived signals: a well-formed draft reads dispatchable=true, authorised=false, ready=false, and the report names which property is absent — § runnabilitypass
Staleness reaches the agentA plan read carries derived age and a staleness verdict computed from the audit's existing thresholds; a 31-day fixture reads stale with effective_status still active and zero blockers, so the verdict is advisory — § stalenesspass
Suite green1,660 → 1,683 → 1,699 → failed at 1,706 → 1,709 after correction → 1,738 (+29) with 0 failures and 0 lint diagnostics against the integrated tree. The failure is the entry worth reading — § suitepass, after a real failure
Draft decay alarmedAll 11 authored-but-unauthorised plans across the 6 projects holding them report an age and the source it came from — 6 from plan-modified, 5 from file modification time — with 0 null ages, 0 unknown verdicts, and the 64-day plan verdicted stale§ decaypass
Wave fence honouredThe mechanism landed — the fence reads computed blocking and gate_blockers — but no wave was actually refused by a gate this session, so there is no transcript to record and the gate is not claimed — § openopen
Own gates migrated5 of the 8 plans done, carrying 48 migrated measures; the other 3 were held because concurrent sessions were mid-landing on them — § migrationopen
Sweep complete1 of 5 repositories done; the rest is held on a dispatch gap, not on the work — § sizingopen

The defect, and why it was one-way

Reckon always derived a plan's blocked state, but the projection could only ever add blocked — never remove a stale one. A plan authored plan-status="blocked" therefore read blocked forever after every blocker cleared, and roadmap knew: it filed the condition as an orphaned-blocked-status finding instead of resolving it.

The repair resolves a legacy persisted blocked to its underlying open state before the projection applies, which makes the same function answer in both directions. Measured, with the former behaviour as the seeded regression:

effective_status('blocked', [])        -> 'active'    # was 'blocked' — never cleared
effective_status('active',  [{...}])   -> 'blocked'   # unchanged
effective_status('blocked', [{...}])   -> 'blocked'   # a live blocker still wins

Writing the status is now refused outright, naming blocked as derived-only, while a legacy document carrying it still reads under a compatibility diagnostic — the posture the legacy-read-posture decision locked. Commits 382501e, 4554a75.

A gate is a declared blocker, so it reuses the derivation

The gate element carries a stable id, the section it anchors to, the sections it gates, a measure, the required evidence, a verdict and an evidence link. Nothing about blocking is new machinery: unpassed_gate_blockers projects every gate whose verdict is not passed into an ordinary blocker row, and the existing path carries it the rest of the way.

gate verdict ≠ passed blocker row kind: gate typed read_plan discovery read_plan roadmap blocking blocking gate_blockers One shared derivation, three consumers. Passing the gate empties the row and every consumer returns to ready — the persisted workflow status is never written in either direction.
Three consumers were the reason the first attempt at this node stopped rather than guessing: read_plan builds blocking in two modules the dispatch had not scoped.

One bidirectional test carries the whole claim. It flips only the verdict, from failed to passed, and asserts that both read paths and ready_now change in both directions while the persisted workflow status stays active throughout. roadmap reports the gate rows as gate_blockers, derives effective_status=blocked, and drops the plan out of ready_now. Commits ca18aef, 54bdf1e.

A gate that can pass without evidence is not a gate

edit_plan gained gate to declare one and pass/fail to record a verdict. A pass carrying no recorded measure is rejected, not warned, while gates.require_evidence is in force, and the refusal names the gate so the caller knows which one it tried to wave through. Commit ca18aef.

Round-trip parity, and one derived field

Gates round-trip byte-identically through the regenerated reckon-owned section, matching the invariant decisions and followups already hold. PlanState carries them, the published schema validates a gates-bearing document and rejects a gate missing its measure, and passed is re-derived from the verdict even when contradictory input supplies passed=true — the same discipline the plan is about, applied to the gate's own state. Commits cbeaa2e, 63cb5a6, bc0881d.

Both halves of unwritable

The sprint-item half landed first: item lifecycle status and implementation fraction derive from plan HTML on read, are rejected on write, and a legacy document reads under a compatibility warning. Measured on a real stale row — an item persisting pending with implementation 0.0 reads active at 0.4, with index.json byte-unchanged. The plan-status half followed in wave four (§ derivation). Commits 4554a75, 382501e.

Concurrency stopped being an error

A lock-guarded invariant used to raise when a second sprint was activated — a single-session assumption that detached worktrees and independent orchestrators had already outgrown. The invariant is gone; the default focus derives from sprint order and incompleteness. Two sprints now hold incomplete items simultaneously with zero audit findings, and because nothing persists the pointer, there is no state left for a test to catch going stale. Starting a sprint ceased to be an operation. Commit 18b0113.

Well-formedness derived, authorisation kept deliberate

The authoring-status gate did one genuinely valuable job — separating written-down from authorised-to-act-on — and one bad one: it decayed silently. Splitting it keeps the signal and removes the decay. dispatchable is derived from checkable properties; authorised stays a deliberate act and is reported with its age. A well-formed draft reads dispatchable=true, authorised=false, ready=false, and the report names which property is absent rather than merely withholding the plan. Commit e570a14.

Detection that reaches the agent

Staleness was already detected and never delivered: reckon audit knew, and no agent reading a plan was told. Derived age and a staleness verdict now ride on the read payload, computed from the thresholds the audit already defines rather than second ones invented for the purpose. A 31-day fixture reads stale while effective_status stays active with zero blockers — advisory, never blocking, which is the whole point: a stale plan is a prompt to re-read its assumptions, not a refusal to work. Commit 67d1cbd.

The wave-four gate failed, and was worth more than the tests

Suite counts across the waves: 1,660 → 1,683 → 1,699 → 1,706 with three roadmap readiness failures → 1,709 after correction. The red run is the useful one.

ready_now had emptied across all ten pending plans, from two stacked causes that every unit test passed straight through:

Both were adapter-shaped — a real derivation, correct in isolation, wrong at the boundary where the public payload is assembled. Verification was moved off fixtures for the re-check: ready_now was confirmed back to 6 against the live project, not a constructed one, and the re-run gate passed at 1,709.

The following wave's five commits then measured 1,738 passing and 0 failing in 48.68 s, +29 on the previous count, with 0 lint diagnostics, against an integrated tree from which all five commits are reachable.

The sweep, sized against the live portfolio

The plan's original figures were written before the scan existed and had drifted. Measured across all twelve mounted projects on 2026-08-12:

What the sweep must clearCountWhere
Live plans persisting plan-status="blocked"12 imas-efit 11, nova 1
Persisted sprint-item statuses46 imas-ambix 36, reckon 8, nova 2
Error-level wiring findings17 imas-efit 16 (including 2 dependency cycles, 8 non-executable and 3 inactive hard dependencies, 3 sprint-order inversions), hanney 1
Wiring findings of all severities82 imas-efit 69, the remaining 13 spread across six projects

Eight of the twelve projects carry no error-level finding at all, so the sweep is concentrated rather than broad: imas-efit is the work, and everything else is small.

One row of it landed, and the rest stalled on something other than the work. This project's own index dropped its 8 persisted item statuses: compatibility warnings went from 8 to 0 while the composed sprint items stayed byte-identical at 4,948 bytes with a matching digest — proof that only ignored state was removed, since the reader was already deriving those values from plan HTML. Commit ea82ad9, 8 deletions and no additions.

Every other row is undispatchable. reckon crew dispatch --repo <other> resolves the node's plan inside the repository it dispatches into and refuses with plan-unavailable, so a node serving a reckon-owned plan cannot edit imas-efit, imas-ambix, nova or hanney — which is the whole remaining sweep. The available workaround is worse than the gap: aiming write paths at a foreign checkout puts a worker outside the worktree that bounds its blast radius, and one of those four repositories had eight concurrent runs writing its state at the time. The sweep-complete gate therefore stays closed, which is the fence behaving correctly rather than a step skipped.

Of imas-efit's 16 error-level findings, only the 8 non-executable-hard-dependency edges are inside the sweep's remit — the scan names their repair, moving a research input from depends_on to informs. The other 8 need a material judgement or a sprint rebalance, both of which the plan's constraints forbid the sweep from making. They are residue to report to the owning project, not work to guess at.

The decay alarm's own evidence was stale

The plan justified the runnability split with "four of eight draft plans across the portfolio are more than two months old, and one carries three locked decisions and a resolved followup". The live scan does not support it, and a gate stated against numbers that no longer exist cannot be met by a correct implementation — so the claim was corrected in the plan before the gate could be attempted.

Measured, 2026-08-12Value
Authored-but-unauthorised plans11, across 6 of 12 mounted projects
Beyond the staleness threshold1 — imas-efit:plan-lifecycle-hygiene at 64 days, verdict stale
Reporting no age at all5 — imas-efit 1, tessera 2, hanney 2

The interesting number is the last one. Nearly half the unauthorised plans carried no recorded modification date, so the alarm could not classify their decay — it reported age_verdict: unknown and said nothing. An alarm that goes quiet on the documents most likely to have rotted is the same failure class the plan exists to remove, which is why the age fallback is a prerequisite of the sweep rather than a tidy-up after it.

Repaired. Every authorisation row now carries an age_source, resolved in precedence order: the recorded plan-modified first, then a discovery creation timestamp, then the mounted plan file's modification time. Nothing is guessed — a row always says where its number came from. Measured across the portfolio before and after:

Authorisation rowsBeforeAfter
Total1111
Reporting a null age50
Naming an age source011 — 6 plan-modified, 5 file modification time
Verdicted stale1 (of the 6 that could be classified) 1 (of all 11)

A recorded plan-modified stays authoritative, so the fallback never overrides a real timestamp; it only speaks where the document was silent. Commit 0183e95, 36 tests passing of which 4 are new.

The fence stopped reading prose, and the documents stopped storing it

The gate fence in the orchestration skill enforced itself by reading a plan's prose evidence-gate table — discipline, not state. It now reads the computed blocking and gate_blockers the derivation produces, with gates.enforce resolved from flight config selecting strict refusal or an advisory warning. The rule's own wording belongs to the plan that authored it and was left byte-identical: the four normative bullets and the refusal sentence are unchanged in the diff, and a new contract test asserts the fence reads computed state and pins out any regression to prose parsing. The prose-table grep count is 0. Commit 3984939, 30 contract tests passing.

The migration then proved the path on real documents. Five plans carry their measures as gate elements instead of tables:

PlanMeasures migratedVerdicts recorded
budget-aware-dispatch2222 passed
uniform-worker-dispatch1111 passed
crew-run-ledger88 passed
flight-control-config77 passed
derived-gate-state (this plan)139 passed, 4 open

Sixty-one measures, no measure dropped, every evidence anchor resolved, and audit-doc reporting no errors across all five. The three remaining plans of the sprint set were held rather than migrated: concurrent orchestrator sessions were mid-landing on all three, writing followups into the same HTML, and racing them would have produced merge conflicts on documents that are the system's own state store.

This plan's own migration is the sharpest evidence that the mechanism works, because it turned the document against itself. With 4 of its 13 gates unpassed, roadmap reports effective_status: blocked, readiness: blocked, the four gate ids by name, and drops the plan out of ready_now — while its persisted workflow status stays active and no status was written anywhere.

What is still open

Three things remain, and none of them is unfinished work in the ordinary sense — each is held by a named condition.

Two mechanisms were found missing while doing the work, and both are recorded as sections on the plan rather than as handoffs: a node cannot be dispatched into a repository other than the one holding its plan, and there is no op that retires the authored prose a gate section supersedes — so every migration in this record went through raw HTML inside a worktree, announced as a bypass each time.