v3.5.3 - bounds not stated, costs not counted #208

Merged
john merged 121 commits from cadence/v3.5.3 into main 2026-08-18 09:33:33 +00:00
Owner

Cadence asserted controls it did not hold. The review path stated bounds it never enforced, the run record claimed to price a run it could not see, and three controls that already existed and were already correct never reached the path that needed them.

Five phases, thirteen requirements (RCL-07, GAT-04, PAR-01, MSR-01, MSR-02, RVP-01, RVP-02, WIR-01, MSR-03, TRN-02, PLN-01, MSR-04, SGT-01), all of it argued off this repo's own trace rather than off a guess.

Bounds the review path never stated: a 4 MiB response ceiling Cadence owns with its own over-response reason and a sanitized 1024-byte failure excerpt; local finding validation pinned to the canonical schema by an 18-fixture agreement table; and the recovery arm renamed to the turn cap and unusable return it can actually produce.

Costs the record never counted: turns per dispatch and per role, the three surfaces pricing a run now naming what that figure excludes, six per-role window ceilings derived from this repo's own p75, a bulk-output file transport with a 17-row register, and workflow.max_plan_tasks re-decided against both its forces and left at 8.

The retune the record could not state: the coordinator residue re-keyed on corr, so one run's marker stops closing at another run's event (phase 2's figure falls from 366,716,303 ms to 3,508,747 ms), and every retune suggestion now carries a direction, a current value and a read target, presented apart from the receipts and ending in an offer to route the change.

Full detail in CHANGELOG.md. Audit clean: 13/13 requirements traced, 35/35 acceptance criteria covered by a UAT item.

The release tag is not in this branch - it is cut on main after this merges.

Cadence asserted controls it did not hold. The review path stated bounds it never enforced, the run record claimed to price a run it could not see, and three controls that already existed and were already correct never reached the path that needed them. Five phases, thirteen requirements (RCL-07, GAT-04, PAR-01, MSR-01, MSR-02, RVP-01, RVP-02, WIR-01, MSR-03, TRN-02, PLN-01, MSR-04, SGT-01), all of it argued off this repo's own trace rather than off a guess. Bounds the review path never stated: a 4 MiB response ceiling Cadence owns with its own over-response reason and a sanitized 1024-byte failure excerpt; local finding validation pinned to the canonical schema by an 18-fixture agreement table; and the recovery arm renamed to the turn cap and unusable return it can actually produce. Costs the record never counted: turns per dispatch and per role, the three surfaces pricing a run now naming what that figure excludes, six per-role window ceilings derived from this repo's own p75, a bulk-output file transport with a 17-row register, and workflow.max_plan_tasks re-decided against both its forces and left at 8. The retune the record could not state: the coordinator residue re-keyed on corr, so one run's marker stops closing at another run's event (phase 2's figure falls from 366,716,303 ms to 3,508,747 ms), and every retune suggestion now carries a direction, a current value and a read target, presented apart from the receipts and ending in an offer to route the change. Full detail in CHANGELOG.md. Audit clean: 13/13 requirements traced, 35/35 acceptance criteria covered by a UAT item. The release tag is not in this branch - it is cut on main after this merges.
john added 121 commits 2026-08-18 09:32:42 +00:00
Cadence holds other people's git rails, so a wrong answer here lands in
someone else's repository as a command that ran or a guard that stayed
silent. That reads critical on the what-if-this-is-wrong test.

shipped was picked against solo back on 2026-08-13 and never against
critical, so this is the comparison that never got made rather than a
reversal.

Repo layer only, so my other projects stay at shipped. It moves two
things: diff off -> blocking, phase_diff off -> adjudicated, and every
role to xhigh (plan-checker jumps sonnet -> opus). If phases stop
feeling parallel it is diff blocking serializing them, and
review.triggers.diff.gate: advisory is the valve.
Two facts copied into docs/WORKFLOW.md drifted from what the code does.
The trigger table said the plan gate is off at shipped; route-table.json
says blocking, and README and METHOD agree with the table. And the
escalation note called escalation unconditional when
model.escalate_on_failure defaults false.

Both are operator contracts, so a reader trusted a gate that was on and
a retry climb that was not. The existing agreement checks pin phase_diff
and route/schema agreement but never covered these copied cells, which
is why it stayed green.
RCL-07, PAR-01 and GAT-04, the three findings that survived adjudicating
the 16 August strict re-review. Ten in, five killed as already-filed,
already-deferred or by-design, one fixed inline as a doc contradiction,
one left open at 17% of its ceiling.

The three share a shape: the control exists, is correct, and does not
reach the path that needs it. RCL-07 is the one that falsifies a stated
claim rather than a documented rail - I reproduced it live here, a recall
for this milestone's own decisions came back with CAPTURE.md hits and
nothing else, hours after the close deleted the phase dirs.

Also seeds the three ids into REQUIREMENTS, which I had left out when the
issues went in.
The risk_surface gate on plan 1's range found two ways the guard reports a
phase already archived when it is not, each followed by the directory removal
that makes the omission permanent.

The label was matched as a prefix of the composed source, so a section named
'<label>/anything' answered a '<label>/' test and marked this close's phases
done. parseArchiveRows now carries label and origin beside source, and the
guard compares the label exactly.

The key was the phase number, so one row proved all three artifacts written: a
close that could not read UAT.md landed two rows, and the retry skipped the
phase whole and removed the directory. The key is the artifact origin now, so a
retry re-reads only what is missing.
The blocking diff gate on plan 1's range: two milestone-prune runs with
different labels both read the same ARCHIVE.md, each writes only its own
label's rows, and the later atomicWrite wins. This seam removes the phase
directories immediately after, so the clobbered rows lose their only other
live source.

withCaptureLock was never CAPTURE-specific - it guards one file's whole
read-modify-write - so it is withPlanningFileLock now, with the refusal reason
as the caller's word since that string reaches a user envelope. The read, the
containment test and the write are one critical section under it, and a refused
lock fails the close BEFORE any directory moves.
An `outcome` event can now name the review trigger it belongs to in a
structured field, so a receipt is joinable to the fire that produced it
without parsing prose. Measured on this repository's 35
`outcome/adjudication` events, the trigger is spelled four different ways
inside the free-text `--detail` and lib/trace-suggest.mjs discards that text
entirely, so the join has to key on something the caller states (D-12).

Refused as bad-args with nothing appended when the flag is present but bare
or blank, the guard `--step` and `--reviewer` already carry: parseArgs gives
a valueless flag the boolean `true`, which would store the literal `true` as
a trigger name. Listed on the `trace append` CONTRACTS row and deliberately
not on `trace close`, which fixes its own family and event.
Proving the detector RAN stopped standing in for proving the blocking fire
HAPPENED. Watched at d30ed50: a record with `checked:true` and
`matches:["secrets","migrations"]` and no outcome event of any kind answered
`ok:true`, exit 0, state `recorded` - a coordinator could read the match,
skip the fire and be cleared.

The per-record shape now carries the category tokens `risk-check run` already
writes and this reader dropped. A record that is `checked:true` and carries
either a non-empty `matches` or `inconclusive:true` is a FIRED range, and it
satisfies the gate only when an outcome event under the same
`rowKey(corr, plan)` names `risk_surface` in the structured `trigger` field
and one of four event names: `adjudication`, `rearm`, `gate_pass`,
`override`. Those are the four outcomes a blocking fire can reach.

Rows gain a fifth state, `unfired`, sitting on top of the four they report
today rather than in place of any: `checked:false` is still `unchecked`, a
widened range is still `stale`, an absent record is still `missing`. Where
every offending row is `unfired` the envelope says `risk-fire-missing` and
its hint names the fire, because telling that caller to re-run the detector
would have it redo the half it already did.
The blocking arm settles at four points and only two of them left anything on
disk. `triage-gate.md` gains the other two - `gate_pass` when nothing
blocker/high survives, `override` when the user clears a FAIL deliberately -
and all four now carry `--trigger <trigger>` plus `--plan <k>` on a per-plan
fire, which is the identity `risk-check status` joins a matched range to its
receipt on.

The PASS receipt exists because a blocking PASS wrote nothing at all: without
it `status` would refuse every matched range whose fire found no blocker, and
an unclearable gate is one that gets bypassed. The override's reason is the
user's own words, so it rides `--detail-file` under the v3.5.2 transport rule
and creates no seventeenth inline site for the scan to classify.

No existing `--detail` value changed - lib/trace-suggest.mjs still parses its
trigger out of the adjudication and rearm details - and both surfaces are
re-pinned in weight-budgets.json in this commit, since each sat at zero
headroom.
The blocking risk_surface gate on plan 2's range found GAT-04's own defect one
level up. Receipts were keyed on the run and the plan alone, so one fire cleared
every later matched range for that plan in the same cycle: run the detector,
fire, fix something, re-run on the widened range, skip the second fire, and
status still answered ok:true. A receipt now names its range on --sha and joins
by head commit; a record carrying no resolved head_id keeps the old join, since
a range no receipt can settle is a gate that gets bypassed.

Two more from the same pass. A non-empty matches whose elements are not strings
was filtered to [] and read as a clean range - it reads as fired now, the same
widening inconclusive already gets. And an override with no reason is refused as
a receipt: it is the one receipt written on the coordinator's own say-so, so a
blank one is indistinguishable from a manufactured clear.

triage-gate.md and review-triggers.md write the --sha; both budgets re-pinned.
Two that survived the narrowed round, with the re-arm cap spent.

The phase-wide arm still cleared a plan on any one satisfying record, so a
later matched range rode in on an earlier fire's receipt - the same defect the
range binding closed on the named arm, one branch over. Every fired record a
row answers for now needs its own receipt.

And the binding was on the head alone, so two ranges sharing a head but
differing at the base cross-cleared, despite being different diffs over
different surfaces. A receipt names both ends: trace append takes --base beside
--sha, and the two reference docs write it.
Two from the blocking diff gate on plan 2's range.

settledBy made --base optional - matched if supplied - which reopened the
widened-range bypass under another name: a fire over B..C settled A..C on the
head alone. Both ends are required now whenever the record carries resolved ids.

And review-triggers.md's adjudication command omitted --plan, so following it
verbatim on a per-plan fire wrote a receipt keyed to no plan. It joined nothing
and left a range that was fired and adjudicated reading as never fired.
The parallel execute branch never called risk-check at all: grep -c
risk-check returned 2 for workflows/execute.md and 0 for
references/execute-parallel.md, so the one gate that is blocking at every
stakes level fired on the sequential path and nowhere else.

Step 5 now runs the sequence per plan after the merges and the worktree
removal, in the main tree - risk-check run on that plan's own
pre-merge/post-merge HEAD pair from step 3, then risk-check status on the
same triple before the plan may be reported done. Everything between the
two calls is POINTED at, not restated: workflows/execute.md's
execute_sequential step is the file this reference is read from and is
already resident, so a second copy would only be two statements of one
rule that disagree at the next edit.
Three checks beside the RSK-01/RSK-02 block, watched failing at e4f95a3
where grep -c risk-check returned 0 for references/execute-parallel.md
against 2 for workflows/execute.md: 20 pass, 3 fail there.

They fail apart on purpose. A tree can call both commands while pasting
execute.md's sequence back in (the fire rule and the transient-file rail
are asserted present there and absent here), while never withholding done
on the status refusal, or while naming a range that is not that plan's
own pre-merge/post-merge pair - and only the third notices a status call
that fell through to the phase-wide arm, which compares no range.
The type still said Set<string> after the value became an array of
{key, sha, base}, so tsc reported three errors against correct runtime code.
Caught by plan 3, which reported it rather than fixing it: planning.mjs was
outside its lease.
risk-check status joins a receipt to a record on rowKey(corr, plan), but
three of the four fenced trace append blocks a coordinator copies verbatim
carried no --plan: gate_pass, override and rearm in triage-gate.md, against
review-triggers.md's adjudication, which did. A receipt appended exactly as
those blocks read keyed to no plan and settled nothing, so the range stayed
unfired and the blocking gate could not be cleared by following its own
instructions. gate_pass is the outcome the common case reaches, which made
the most-travelled clearing path the one that did not join.

The rule was already stated in prose beside the blocks. A prose rule next to
a command that disobeys it is exactly the drift prose-agreement.test.mjs
exists to catch, so the new check reads every fenced --family outcome
invocation in both references and demands --trigger, --plan, --base and
--sha on each, plus all four settle points present so a deleted block cannot
pass vacuously. Nothing pinned this before: risk-diff.test.mjs's receiptLine
helper builds its fixtures with a plan every time, so the seam's own tests
were handed what these documents omitted.

Watched failing at dd3920e (exit 1, 23/24, on gate_pass drops --plan).
weight-budgets.json re-pinned 8726 -> 8759 for the three added flags.
The measurement half of v3.5.3's cost work, split from the two requirements
it unblocks. MSR-01 records the tool-call count the subagent return already
carries and trace close discards; MSR-02 stops /cad-report and /cad-suggest
pricing a run from worker-return tokens alone. MSR-03 and PLN-01 stay unplanned
until those figures exist, since both are arguments from a record that cannot
currently hold them.
The record carried only what a worker RETURNED, and a run's price is turns
times window - so a token figure alone can describe what came back and never
what it cost to get there. `--turns` is the tool-call count read off the same
subagent return metadata `--tokens` is, validated in the ONE shared
`append|close` body the way `--raised` is: `requireInt` plus a non-negative
test, no comma-grouping exception (a tool-call count is never printed grouped),
and a malformed value is a malformed CALL that appends nothing at all rather
than a best-effort append with the field dropped.

Carried onto the event through the same conditional spread the other optional
fields use, so an absent figure adds NO key rather than a `0` - a zero would
claim a dispatch that used no tools, while an absent key is readable as a host
that reported none. A real `--turns 0` still records a 0.

Listed on the `trace close` CONTRACTS row only, exactly as `--raised` is
listed on `append` only: the row is a prose allowlist and never widens what a
subcommand accepts.
`renderTrace` accounts for turns the way it already accounts for tokens, and
only that way. A paired bracket row carries the turn figure, preferring the
terminal's and falling back to one the dispatch half held. Each role row gains
a turn TOTAL and `turns_unrecorded`, a counter of its own: the existing
`unrecorded` keeps meaning "no token figure was reported", so a dispatch that
reported tokens but no turns stays distinguishable from the reverse. Collapsing
the two into one scalar would have falsified progress.md's stated
absent-prints-unrecorded rule with no test going red.

The accumulator gets its own recorded/figures pair beside the token one -
`turnsFigures` alone decides whether a total is emitted, so an unmatched
terminal's real figure is not dropped, and a separate funded-once flag stops
one dispatch being counted twice on either side. The terminal replay identity
now discriminates on the turn figure too: two closes differing only in their
turn count are two closes, not one replay.

Both turn keys are gated on a figure having landed, which is where they part
from the token pair - a `turns_unrecorded` emitted on `unrecorded`'s footing
would put a new key on every role of every trace written before the flag, and
the committed verbatim fixture's rendered `roles` is pinned byte-for-byte as
the proof this change is invisible on an old record.

A non-numeric or non-finite value contributes nothing, exactly as `tokens`
does. `TraceRender` and the `roles`/`brackets` JSDoc state the shape.
The paragraph that already states the ONE `trace close` per dispatch moment
rule now also carries `--turns`: the tool-call count on the same subagent
return, omitted when the return carries none rather than sent as zero, and a
turn-figureless return stated as ROUTINE, rendering under `turns_unrecorded` -
a counter distinct from the token `unrecorded`, so a reader can tell a
dispatch that reported tokens but no turns from the reverse.

Extended, not copied: still ONE statement, and the provenance argument stays in
lib/trace.mjs's TOKEN PROVENANCE header rather than being duplicated into eager
prose.

The three checks the task's Verify names all pass on the UNPATCHED tree - the
file sat at its pin, self-verify was green, and the paragraph already held one
`ONE statement` - so they prove the re-pin and nothing about the prose. The
prose-agreement check is what goes red without the content: it reads the
paragraph by its bolded anchor and asserts the carry, the omission and the
separate counter, and was watched failing with each of those three sentences
deleted in turn (exit 1, one failing test each time).

seams.md re-pinned 19258 -> 19726 in this commit.
All TEN shipped `trace close` invocations carry `--turns` now, in this phase
and not a pilot subset: context.md 1, plan.md 2, plan-revision.md 2,
review-triggers.md 1, execute.md 1, decision-review.md 1,
minimalism-review.md 1, verify-deep.md 1. A partial rollout would put a second,
unstated rule in the tree - "some roles report turns" - and a per-role total
would then read as low rather than as partial.

Each site carries it in the same placeholder register its `--tokens` already
uses, the figure on the subagent return, so a coordinator copying one number
copies the other from the same place. No site gains a paragraph and no site
restates the provenance argument: seams.md's bracket paragraph is the ONE
statement and the sites point at it.

weight-budgets.json re-pinned for all eight files in the same commit.
`traceCalls` reads `--turns` off a close line the way it already reads
`--tokens`, and the loop that asserts `--role` and `--plan` on every close now
asserts the turn count too. This is what holds the all-ten conversion
mechanically: the per-file map already requires closes to EQUAL dispatches, so
it sees a lost bracket, but nothing saw a site quietly shedding the flag - and
turns per role would then read as LOW rather than as partial, which is the
conflation the separate counter exists to prevent.

No per-arm or per-file count of its own: the BRACKETING map already binds the
counts and a second copy of that arithmetic is what the census's own header
refuses.

Watched red: deleting the flag from context.md's single close line fails the
census naming `cadence-core/workflows/context.md`.
One falsifier exercising MSR-01 end to end through the CLI alone: a close
carrying a turn figure, a close carrying none, four malformed values, and a
render reading all of it back. It asserts the three properties the acceptance
criteria name - the figure persists onto the event and reaches both the bracket
row and the role row; a figureless close lands under the turns counter of its
own and never as 0; a malformed value appends nothing at all.

Nothing this plan added is imported, so against the unpatched tip it fails on
its ASSERTIONS rather than on a missing export. Watched failing at 97eaf03,
where the seam accepted `--turns 83`, reported `written:true`, wrote a line
with no `turns` key and rendered the dispatch priced by tokens alone - and
where `--turns -1` also returned `ok:true`, which is the assertion it dies on
first. The header quotes that observed output and carries the re-watch recipe.
The row asserted the four family counts sit under one `corr` and marked
that accurate, which is the claim D-08's twelve-corr measurement falsified
and PROGRESS-28 was rewritten for in this phase. D-13 enumerated the rows
to move by name and this one was not on the list, so the sweep passed over
a row two lines above one it did rewrite.

The tail is dropped rather than restated: the scoping fact it stated wrong
is PROGRESS-28's own subject, so repeating it here would put one claim in
two rows. The anchor moves to 99-101 with the sentence it cites, and
PROGRESS-13 is re-anchored 93 -> 95, the drift D-13 names as its example.
Nine items over AC1-AC7, eight auto-verified by the deep pass and the ninth
its own gap: PROGRESS-14 still recording the falsified one-corr claim,
fixed in 9b96b08 and retested here.
risk_surface blocked plan 1's range on auth and secrets. Three of the four
survivors are fixed here; the fourth is recorded open.

req.destroy() during an active response aborts the IncomingMessage too, and
res carried no 'error' listener - so the byte ceiling could take the process
down instead of answering reason:"over-response". The fake transport emitted
on req alone, which is why the plan's own tests passed; it now aborts the
response the way a socket does, and the flood case is the regression test.

The credential sanitizer stopped at the first space inside a quoted value,
leaving all but the first word of a multi-word secret in the excerpt, and could
not see a camelCase key at all - apiSecret and clientSecret went through
byte-identical. A quoted value now runs to its closing quote, and a
case-sensitive second pattern covers the camelCase spelling without letting
monkey or turkey match on their last three characters.
FINDING_SCHEMA gains the four constraints the finding shape has always
implied and never stated: minimum 1 on line, minLength 1 on file, claim and
failure_scenario, a maxLength on each of those three, and maxItems on the
findings array. The numbers are sized against what this tree has produced -
file 39 chars, claim 159, failure_scenario 376 in the one committed findings
file, and a largest panel round of 9 - not picked round.

No minItems: an empty findings array is what a reviewer that found nothing
returns, and refusing it would turn a clean review into a bad-shape
degradation. None of OpenAI's structured-output unsupported set rides in
with them, and stripAdditionalProperties stays the Gemini-only carve-out it
is - the new test asserts both, plus that the keywords reach all three
dialects: strict json_schema, responseSchema, and DeepSeek in-prompt.
Every constraint FINDING_SCHEMA now carries is mirrored locally with its own
diagnostic naming the offending field and the bound it crossed: line below 1,
an empty or over-long file/claim/failure_scenario, a findings array past
maxItems, and an unknown key at BOTH levels, since additionalProperties:false
sits at both. Never a shared 'invalid finding' string - this text reaches the
user as {ok:false, reason:'bad-shape', detail}, and a degradation the user
cannot act on is the silent drop this guard exists to end.

Lengths count Unicode CODE POINTS, as JSON Schema specifies. A .length
reading counts one emoji as 2 and would refuse a string the schema accepts;
the test pins both directions against a non-BMP fixture.

The existing five diagnostics and their first-defect-wins order are intact,
and an EMPTY findings array is still accepted - that is what a reviewer with
nothing to report returns.

bodyOfBytes moves its filler from the finding's claim to a sibling key of
output_text. Padding the claim stopped being a valid finding under the new
2000-character maximum, and a response-ceiling fixture that degrades to
bad-shape proves nothing about the ceiling.
The second reading of FINDING_SCHEMA that makes the pairing with
validateFindings a machine-run fact rather than a reviewer's opinion frozen
at write time. It implements exactly the keywords the schema uses - type,
properties, required, additionalProperties, items, enum, minimum, minLength,
maxLength, maxItems - and THROWS on any keyword outside that set, over the
whole schema tree rather than only the branches a value reaches. An ignored
keyword evaluates as satisfied, which is how an agreement test goes green on
an agreement it never checked.

Lengths count Unicode code points. Both sides of the comparison are written
in this phase, so a shared .length reading would agree with itself while
disagreeing with the schema; the non-BMP cases pin it in the accepting
direction, which is the one a UTF-16 count gets wrong.

Test-only by design, but it lives under lib/ rather than in the test file
because tsconfig.ci.json excludes *.test.mjs - and this helper's correctness
is what the agreement verdict rests on.
Eighteen fixtures, each run through the keyword-limited evaluator against
the LIVE FINDING_SCHEMA and through validateFindings, asserting the two
agree on accept-vs-reject. No hand-paired expected verdicts: a human-written
schema column is asserted by reading, and the drift returns unnoticed.

Two of the rows are non-BMP - a claim of exactly maxLength astral characters
(accept) and one past it (reject). Both sides of this comparison were
written in the same phase, so a shared UTF-16 .length reading would agree
with itself while disagreeing with the schema, and every BMP-only row is
blind to that.

Watched failing in both directions before this landed: deleting `minimum`
from FINDING_SCHEMA gives 'line: 0: FINDING_SCHEMA says ACCEPT but
validateFindings says REJECT', and deleting the mirroring check from
validateFindings gives the same fixture from the other side.
The wire pin gains D-07's research, so the next person to touch
FINDING_SCHEMA does not re-derive it: minimum, minLength, maxLength and
maxItems ride the wire unchanged on all three shipped providers, checked
2026-08-17. OpenAI's structured-output unsupported set is named in full so a
future keyword is refused before it is sent, Gemini's OpenAPI subset
documents the numeric and array bounds explicitly, and DeepSeek enforces
nothing server-side - which is what makes local validation the guarantee
everywhere regardless.

The file sat at exactly its 5048-byte pin with zero headroom, so the budget
is re-pinned to 6304 in the same commit; self-verify reported the overrun
before the re-pin, which is the falsifiable half.
One falsifier exercising the requirement end to end: three provider answers
the canonical schema refuses - a zero line number, an empty claim, a key
nobody declared - each of which must degrade to bad-shape with a diagnostic
naming the field.

WATCHED FAILING AT 15b5d4c, the tip of this plan's unpatched tree, where all
three resolved ok:true and went to a human for triage as if they were
findings. The header quotes that output verbatim and carries the re-watch
recipe, which copies lib/schema-eval.mjs as well as this file - the
evaluator cases earlier in the file import it, and a checkout without it
fails at module resolution before any assertion runs.

Full suite 2106 pass, self-verify ok:true, tsc clean.
Forgejo Actions serves it from the self-hosted remote's own badge endpoint for
.github/workflows/test.yml, so the status is this repo's, not a mirror's.
CRED_VALUE matched a quoted value only when its closing quote was present, and
the bare class excludes quotes, so a credential straddling bodyExcerpt's
4096-byte sanitize window survived byte-identical - 73 bytes of a password
value reached the http failure envelope in the measured case. It now also
matches an unterminated quoted value to end-of-input, with the terminated forms
tried first so a well-formed body is untouched.

bodyExcerpt's docblock claimed the window edge could not reach the excerpt
because the trailing token is dropped at the last whitespace. That arm is gated
on clean <= room, which a body of almost entirely credential spans skips. The
comment now names the real guarantee and keeps the whitespace arm as a second
line of defence.

Regression fixtures at both levels, each watched failing against the unpatched
helper: the unit case in redact-url.test.mjs and the straddling-window envelope
case in review-provider.test.mjs.
check 1b's placeholder fixture gains the two-segment family token
workflow.max_dispatch_tokens, and check 12's independent mergeLayers(
callsite census rises 12 -> 13 for the trace window arm's resolve.

Landed under an explicit lease grant: PLAN-2 otherwise leases this file,
and its files: row is recorded in PLAN-1's frontmatter.
read_record takes a third seam call, planning.mjs trace window, on the same
--phase scope as the render; compose gains a Window budget line reporting each
crossing as role, dispatch, figure and the ceiling it crossed, with unbudgeted
roles and unrecorded returns beside it.

The crossing is a FINDING and refuses nothing - the dispatch it names already
returned - and it is read off the same brackets[] tokens key the gap terms
already describe, so the command acquires no second window number. The figure
and the ceiling print as two numbers, never their quotient.

report.md re-pinned in weight-budgets.json in this commit (D-20).
One CLI-only case: a crossing bracket and an under-ceiling bracket in one
scratch .planning, asserting the crossing comes back in the budget-overrun
shape with the role named and the under-ceiling dispatch is compared but
silent. It imports nothing this plan added, so at 9b1fe53 it dies on its
first assertion - trace <append|close|render|suggest|ignore> - rather than
on a missing module.

The header carries WATCHED FAILING AT 9b1fe53, the observed unpatched output
of both the seam call and this case, and the re-watch recipe.
The purpose string still read as a per-PHASE refusal - a task count above
which a plan is not written - while workflows/plan.md:139 and
references/config-catalog.md:30 both define max_plan_tasks as a per-PLAN
ceiling that a phase answers by taking more plans, and reserve
`## PHASE TOO BIG` for a phase no number of plans of that size delivers.
A reader configuring the key off the schema would raise it to avoid
refusals that never happen. The phase's own agreement test compares the
extracted numbers and not the behaviour, so it passed over the
disagreement.
The residue accumulators in renderTrace key on `corr` instead of `key(e.phase)`
at all three sites - the end-of-record `last` update, the COORDINATOR marker
collection and the paired-bracket `spans.push` - so each run's last marker
closes at that run's own last event rather than at whatever event a later run
sharing the phase number happened to write. `mergeSpans` and the clip-and-
subtract arithmetic inside a window are untouched; what moved is which markers
and brackets land in one row and where the LAST marker's window ends. A marker
that is its run's last event now closes at itself and contributes a zero-length
window - the record holds no evidence the coordinator kept working after its own
last act. A `steps[]` row's `phase` now comes from the marker's own event, since
the key is no longer a phase; the row's key set is unchanged.

The figures, measured over ONE record (the live `.planning/trace.jsonl` copied
into a scratch `--dir` and rendered by both d94c79d's `planning.mjs` and this
tree's), `trace render --phase 2`:

  coordinator.residue_ms   366,716,303 -> 3,508,747
  largest steps[] window   `commit` @2026-08-13T20:30:13.500Z, 280,613,472 ms
                        -> `acceptance_criteria` @2026-08-12T19:35:12.762Z,
                           1,081,370 ms

The 4,677-minute `commit` window was opened by `2-6790224`, a run whose own last
event is 25 seconds later; phase "2" holds 9 distinct `corr` ids spanning
2026-08-08 to 2026-08-17. That is the measurement that refutes the roadmap's
"unattributable" reading: the figure was wrong, not unattributable (D-01).

The three shipped readers move in this same commit (D-17): `workflows/report.md`
states the residue as the exception to its own phase-pooling warning and keeps
saying it is TIME and never tokens; R6's comment in `lib/trace-suggest.mjs` says
the figure it relays is corr-scoped, with its `evidence` string and `action:
null` byte-identical (D-02); and `trace render`'s usage header in `planning.mjs`
stops implying a phase span. The shipped test asserting the old pairing rule
changes name, comment and arithmetic together (9 min -> 6); the four single-run
coordinator fixtures pass unedited. `report.md` re-pinned at 12883 and
DOCS-CLAIMS REPORT-13/14/15 re-anchored - REPORT-13's 67-69 was already stale.
The MSR-04 falsifier. One phase, two runs: run A anchors, marks, brackets a
worker and marks again, its record ending two minutes later; five hours of clock
pass; run B anchors under the same phase NUMBER and writes its own events. The
assertion is the invariant AC5 pins rather than a figure - for every `steps[]`
row, the window may not exceed the tail of the `corr` that opened it - because
this repository's trace grows while the phase runs and no residue is stored
anywhere.

WATCHED FAILING AT d94c79d with this file copied into that checkout:

  x falsifier: every step window closes inside the corr that opened it (MSR-04)
    AssertionError: `commit`: a 18180000 ms window opened by `1-aaa1111`, whose
    own record ends 120000 ms after the marker - the window closed at another
    run's event.
  i pass 108
  i fail 2

18,180,000 ms is 303 minutes of clock across a boundary nobody worked over. The
second failure there is task 1's re-pinned two-corr fixture, the same fact read
from the other end, and the header says so. The header also carries the re-watch
recipe; the SHA is what git reported at execution time.
The suggest arm's mergeLayers call, added in 36841d5 to bind and ride its
warnings, is the fourteenth callsite in the live tree. The rule was already
satisfied - the callsite destructures warnings and self-verify reports no
undocumented-merge-warnings problem - only the pinned total was stale. Re-pin
it, and state in the title the numbers the check actually asserts.
R3 shipped `proposed` as the rung the record's escalated resolves landed on
without ever comparing it to the rung a config layer set, so a user who had
already applied the retune got back `raise from xhigh to xhigh` - a suggestion
naming no change, which is the exact mute output SGT-01 exists to kill - and a
record whose retained escalations landed lower could propose a rung UNDER the
one in force while still saying raise.

`raiseTarget` compares the target against the value in force on the rung
ladder `route-table.json` states, on the terms `oneStepDown` already set for
gates: strictly above or the target is omitted, an unset key keeps it because
nothing is in force to contradict, and an absent ladder omits rather than
substituting an order from memory. The suggestion itself still stands - it is
the target that goes quiet, not the advice.

Found by the blocking risk_surface gate on the plan-2 range.
`workflows/suggest.md`'s `present` step stops printing one list. The `suggest`
entries are numbered under a heading of their own - subject, config key,
`current`, `direction` with the `proposed` target beside it, and the evidence
verbatim - and the `info` receipts sit under a second heading below, never
interleaved with them. Interleaving the two kinds is what made a retune read as
a report: a receipt that asks for nothing looked exactly like a tweak that does.

Two absences are stated rather than blanked, because the seam returns them that
way on purpose. An entry with no `proposed` says the record cannot price a
target, since a blank is indistinguishable from a forgotten field. An unset
`current` is relayed in the form the seam returns, naming the stakes level that
decides it and never the value that level would fire - printing the effective
value invites a set that pins the gate at every stakes level.

The empty arm is new: a return carrying `info` entries but no `suggest` entry
still prints the tweak heading, with one line saying the record supports no
tweak in this scope and nothing attached to it. `thin_record` is untouched - it
replaces `present` entirely when `suggestions` came back empty, which is a
different state. The relay-unchanged rule and its arguable-denominator paragraph
are byte-identical: this step changes the SHAPE of the presentation and
recomputes nothing.

`read_record` also names `warnings`, which the suggest arm's `mergeLayers` call
now rides on the envelope (D-13) - an unreadable config layer is why a `current`
can be reading less than the project set.

suggest.md re-pinned at 6788, and eleven `SUGGEST-*` claim rows move with the
edit: 07 for the widened envelope list, 08 for the longer suggestion key list,
09 for the receipts heading, and 10-17 re-anchored where the rewrite shifted
them.
"Then STOP - apply NOTHING ... There is no apply arm here to decline" is gone.
The `present` step now closes by turning every tweak that came back with a
`proposed` target into a `/cad-config <key>=<value>` token and ASKING whether to
run `/cad-config` with them, through the SlashCommand tool. The tokens are named
in the question itself: a user answering yes to a summary has not seen the
values. A tweak the seam could not price has no token, is named without one, and
no value is synthesized for it; when nothing is priced there is nothing to
route, so the step says so and asks nothing.

Every posture the deleted sentence was protecting is stated where the offer is:
the suggestion is input to the user's decision as triage-gate.md holds review
findings, nothing is applied on the way to asking, an unanswered offer means no
change, and the ask-user seam's open-ended arm is the binding - the turn ends on
the question and the answer is never assumed. The write, on a yes, happens
INSIDE `/cad-config`, which takes `<key>=<value>` tokens directly, so this
command still writes no config key itself and the hand-edit route stays open.
The first guardrail says that in those terms rather than claiming a read-only
posture the offer contradicts; the no-fabricated-figures and no-phantom-key
bullets are untouched.

`skills/cad-suggest/SKILL.md` now declares what the prose actually names and
nothing else: `SlashCommand` for the route, `Bash` for the one seam call. `Read`
goes, because no step reads a file - the seam is the only reader of the trace,
which is the workflow's own rule, and the workflow @-include is eager prose
rather than a Read. The objective and the description drop "apply none of them"
for the offer, keeping the true half: the command relays the record, never
recomputes it, and writes no config key itself.

suggest.md re-pinned at 7823 and the skill at 1044. SUGGEST-11, 12 and 16 carry
the apply posture and are re-claimed to what the file now says; 13, 14, 15 and
17 are re-anchored where the offer shifted them.
Three things change in `/cad-milestone`'s retune sentence and nothing else in
that paragraph does. It names `/cad-suggest`, the command a reader can run,
rather than `trace suggest`, the seam under it - the defect CAPTURE.md recorded
in phase 3. It says each tweak carries its config key, the value in force, a
direction and a target value where the record can price one, rather than only a
key and receipts. And it states the offer instead of claiming the command
"applies none of them without your say", which the offer arm makes misleading:
the command writes nothing itself and ends by offering to route the tweaks you
accept to `/cad-config`. The gate whose fires kept coming back empty and the
role that never needed its escalation stay - they are accurate and they are what
make the claim concrete.

The `/cad-suggest` bullet in the command list made the same "applies none of
them" claim and named no direction or target, so it moves with the sentence for
the same reason. `applies none` now appears on exactly one README line, the
`/cad-minimalism-review` bullet, which this does not touch.

Claim row README-74 moves with both: its anchor was already stale at line 97
against a sentence that sits at 99, and its claim text now describes the
sentence as it stands. No `weight-budgets.json` row covers README.md, so no pin
moves here.
docs: open v3.5.4 - the gate that clears itself wrong
All checks were successful
test / node-test (git, 22) (pull_request) Successful in 47s
test / node-test (git, 24) (pull_request) Successful in 21s
test / node-test (other, 22) (pull_request) Successful in 24s
test / node-test (other, 24) (pull_request) Successful in 27s
test / node-test (planning, 22) (pull_request) Successful in 1m13s
test / node-test (planning, 24) (pull_request) Successful in 1m8s
test / node-test (prose, 22) (pull_request) Successful in 26s
test / node-test (prose, 24) (pull_request) Successful in 24s
test / node-test (review, 22) (pull_request) Successful in 15s
test / node-test (review, 24) (pull_request) Successful in 15s
test / node-test (routing, 22) (pull_request) Successful in 22s
test / node-test (routing, 24) (pull_request) Successful in 25s
test / self-verify (pull_request) Successful in 12s
test / typecheck (pull_request) Successful in 15s
829f400c6b
john merged commit f4bec975d1 into main 2026-08-18 09:33:33 +00:00
Sign in to join this conversation.
No description provided.