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
| Measure | Evidence | Verdict |
|---|---|---|
| Two-way derivation | effective_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 —
§ derivation | pass |
| Gate blocks and unblocks | One 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 — § mechanism | pass |
| Evidence-free pass rejected | A pass op with no recorded measure
is refused while gates.require_evidence holds, and the refusal names the gate —
§ evidence | pass |
| Blocked unwritable | A 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 —
§ unwritable | pass |
| Round-trip parity | Gates 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-trip | pass |
| Concurrent sprints expressible | Two 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 — § concurrency | pass |
| Runnability derived | dispatchable 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 — § runnability | pass |
| Staleness reaches the agent | A 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 — § staleness | pass |
| Suite green | 1,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 — § suite | pass, after a real failure |
| Draft decay alarmed | All 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 —
§ decay | pass |
| Wave fence honoured | The 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 — § open | open |
| Own gates migrated | 5 of the 8 plans done, carrying 48 migrated measures; the other 3 were held because concurrent sessions were mid-landing on them — § migration | open |
| Sweep complete | 1 of 5 repositories done; the rest is held on a dispatch gap, not on the work — § sizing | open |
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.
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:
mcp._inventory_rowstripped gates and followups beforebuild_roadmapsaw them, soopen_followupread as missing on plans that plainly had one. Fixed in 27 lines — commit7ae4fa3.- Gate-based dispatchability stranded every not-yet-migrated document, which the locked
legacy-read-posturedecision forbids: rejection was always meant to apply to writes only. Fixed in 8 lines — commit7770425.
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 clear | Count | Where |
|---|---|---|
Live plans persisting plan-status="blocked" | 12 | imas-efit 11, nova 1 |
| Persisted sprint-item statuses | 46 | imas-ambix 36, reckon 8, nova 2 |
| Error-level wiring findings | 17 | 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 severities | 82 | 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-12 | Value |
|---|---|
| Authored-but-unauthorised plans | 11, across 6 of 12 mounted projects |
| Beyond the staleness threshold | 1 — imas-efit:plan-lifecycle-hygiene
at 64 days, verdict stale |
| Reporting no age at all | 5 — 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 rows | Before | After |
|---|---|---|
| Total | 11 | 11 |
| Reporting a null age | 5 | 0 |
| Naming an age source | 0 | 11 — 6 plan-modified,
5 file modification time |
Verdicted stale | 1 (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:
| Plan | Measures migrated | Verdicts recorded |
|---|---|---|
budget-aware-dispatch | 22 | 22 passed |
uniform-worker-dispatch | 11 | 11 passed |
crew-run-ledger | 8 | 8 passed |
flight-control-config | 7 | 7 passed |
derived-gate-state (this plan) | 13 | 9 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.
- Wave fence honoured. The mechanism landed, but no wave was actually refused by a gate this session; the two waves that did not open were held by a dispatch gap and by concurrency instead. Recording a manufactured transcript would defeat the gate, so it stays open until a real gate refusal happens.
- Own gates migrated. Three plans of the sprint set were being written by other sessions throughout. The migration needs a quiet window on those documents, not more effort.
- Sweep complete. Held on the cross-repository dispatch gap recorded on the orchestration plan. The survey is done and written down, so the work resumes from an inventory rather than a re-scan.
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.