v3.5.5: a seam that accepts what it should refuse #225

Merged
john merged 123 commits from cadence/v3.5.5 into main 2026-08-19 20:39:48 +00:00
Owner

Closes the v3.5.5 milestone. Thirteen requirements across five phases, 123 commits.

Readers that accept what they have a rule against, closed by replacing nine hand-rolled argument parsers with one declarative table: 16 scripts, 77 subcommand rows, 145 flag entries, plus a census that spawns the owning binary for all 231 declared refusals. That census is what caught the table being written but not read, planning.mjs was applying two of its 145 rules.

Also in here:

  • a string "" in git.protected_branches had been unprotecting main silently
  • --dir '' read as "use the current directory" at six seams
  • a bare trailing --date dated the release today
  • detect-commands named binaries that are not on PATH
  • risk-check status could not be satisfied for a non-numeric worker key
  • the risk_surface detector matched its own source and fixtures
  • ## Shipped and ## Traceability lookups read inside fenced code blocks
  • README rewritten from a 24,850-byte reference manual to a 14,433-byte decision document, with the relocated claims under CI

Audit green: 13/13 traced, 0 broken, 35/35 acceptance criteria covered. 2380 tests pass, self-verify clean. Manifest at 3.5.5; the tag gets cut on main after this merges.

References #219, #223, #224.

Closes the v3.5.5 milestone. Thirteen requirements across five phases, 123 commits. Readers that accept what they have a rule against, closed by replacing nine hand-rolled argument parsers with one declarative table: 16 scripts, 77 subcommand rows, 145 flag entries, plus a census that spawns the owning binary for all 231 declared refusals. That census is what caught the table being written but not read, `planning.mjs` was applying two of its 145 rules. Also in here: - a string `""` in `git.protected_branches` had been unprotecting `main` silently - `--dir ''` read as "use the current directory" at six seams - a bare trailing `--date` dated the release today - `detect-commands` named binaries that are not on PATH - `risk-check status` could not be satisfied for a non-numeric worker key - the `risk_surface` detector matched its own source and fixtures - `## Shipped` and `## Traceability` lookups read inside fenced code blocks - README rewritten from a 24,850-byte reference manual to a 14,433-byte decision document, with the relocated claims under CI Audit green: 13/13 traced, 0 broken, 35/35 acceptance criteria covered. 2380 tests pass, self-verify clean. Manifest at 3.5.5; the tag gets cut on `main` after this merges. References #219, #223, #224.
john added 123 commits 2026-08-19 20:39:16 +00:00
Scoped from the tracker milestone v3.5.5, which holds twelve issues. Six of them
(#219 through #224) were filed at this open, out of a full audit of the capture
queue: 222 open items walked, 53 already closed by a shipped fix, and every
defect below re-verified live rather than trusted from its note.

The theme is the door rather than the path. v3.5.4 closed the shape for a control
that reaches its path and mis-answers; this cycle takes the argument face that
says yes to input it has a rule against.

Phase 1 carries the two that remove a protection - #219 unprotects every branch,
#223 lets one repository answer another's blocking gate. Phase 2 carries the five
readers that accept malformed input and answer anyway. Phase 3 carries the gates
that fire on themselves or cannot be satisfied. Phase 4 is the structural form of
phase 2 and goes last on purpose, because a declarative contract is a guess until
the case-by-case fixes have said what it must express.

Twelve requirement ids seeded: GRD-01, SCR-01, ARG-01 through ARG-06, RCH-01,
RSK-03, RSK-04, SHP-01.
Promotes the 2026-08-18 capture decision into v3.5.5 as its fifth phase,
requirement RME-01. The decision was already fully specified in the capture
queue: what stays on the landing page, what moves to docs/, and the wording
rule for the audience section. Nothing about it is re-opened here.

The phase is off-theme for this cycle and the Overview now says so rather
than letting the ordering sentence imply a fifth defect phase. It shares no
code with the four and depends on none of them.
Two plans, because the phase carries two defects that share no source:
PLAN-1 is GRD-01 in lib/protected-branches.mjs and its five callsites,
PLAN-2 is SCR-01 across the six scratch-transport sites plus a new lint.
They are sequential rather than parallel: both re-pin a prose surface in
weight-budgets.json, which is the one file the CONTEXT shape directive did
not account for.

The blocking plan review returned two highs. The split-site read-backs
guarded truncated and malformed files but nothing proved the file at a
carried path belonged to this run, so a well-formed record from an earlier
run still answered; tasks 4 and 5 now carry a run token compared against an
id held outside the file. AC6 is amended to accept the seam envelope for
review-provider.mjs --payload rather than ask one seam to break the
one-JSON-line-on-stdout convention for a property already tested there.
A string `""`, a whitespace-only string and an array whose entries all name
no branch resolved to a list that read as configured and matched nothing -
`[""]` guards no commit, and two of the five readers index [0] for a base ref,
so it also spent itself as `git branch --merged ""` and `git log ..HEAD`.

One predicate now decides both spellings (D-01): an entry survives only when
it is a string whose trimmed form is non-empty. A non-empty value left with no
survivors falls to the default `['main','master']` (D-02, a value naming no
branch is a typo), while `[]` still passes through untouched (D-09) and a
surviving entry is kept exactly as the user spelled it.
A commit on `main` under `git.protected_branches: ""` came back null before
the fix - the key that names the protection was what removed it. The row
asserts the ask decision and the refuse alias, matches the reason naming
"main", and carries the array spelling `[""]` beside it as the same grammar
reached two ways (D-01). The `[]`-is-unguarded row is untouched: it holds the
opposite answer for the opposite input.
`base` falls back to protectedBranches[0], so an out-of-grammar value moved a
BASE REF and not only a guard verdict: under `""` the reap query became
`git branch --merged ""`, which answers emptily and successfully. The row
asserts base lands on `main`; against the pre-fix resolver it reports `''`.
The same [0] base ref, spent on `git log <base>..HEAD`: under `""` the range
became `..HEAD`, git read the empty side as HEAD, the log came back empty and
the tracker report named no issue while still answering ok:true. The row
asserts the issue NUMBERS rather than the action - the pre-fix tree also
answers action "report", and `referenced: []` is exactly the defect.
The row named three and has been stale since git-branch.mjs and
issue-check.mjs began reading the same coercion. It now names all five and
points at bin/lib/protected-branches.mjs as the one place the grammar lives,
so a sixth reader finds the module rather than a callsite to copy, and says
that two of the five index [0] for a base ref. Reach stays universal. The
budget entry is re-pinned in the same commit, since re-pinning later is the
drift budget-overrun exists against.
The three JSON read-backs tested shape BY dereferencing the parsed value,
after the try/catch had closed: JSON.parse("null") is a successful parse, so
`r.outcomes`, `r[k]` and `r.brackets` threw an uncaught TypeError where the
prose promises a named scratch-shape refusal. The shape check WAS the crash,
so no site could reach the refusal.

Guard the parse result before touching it, and guard the array elements the
same way: an outcomes or brackets entry that is not an object is refused
rather than dereferenced, and a printed field that is present but null is as
absent as a missing one. Without the element guard, [42] answered a blocking
re-arm gate with a fabricated 0 and [null] threw.

scratch-readback.test.mjs pins all six arms by executing the shipped scripts
against the fixtures, so nothing here rests on reading the prose.
The v3.5.5 open inserted its own Active section above the v3.5.4 one and left
that section's body behind, taking only its first two lines. What survived
began mid-sentence at "## Shipped` below." - a fused heading that read as a
section break where none exists - and restated the deferred ids, the milestone
scoping and the seeding rule for a cycle that had already closed.

Delete the orphan and carry forward the two facts the live section did not
already state: the unassigned open items from the v3.5.1 and v3.5.2 closes, and
the eight medium risk_surface survivors from v3.5.4. The title tracks the open
cycle rather than the closed one.
`### Active` still opened "No cycle open" and described the v3.5.4 close, while
REQUIREMENTS and ROADMAP had carried v3.5.5 since 2026-08-18. That section is
where `planning.mjs audit` reads the doc version, so the stale line was firing
version_drift against the already-published v3.5.4 tag - the FRI-03 signal
reporting an interrupted close that had not happened.

State the open cycle first and keep the v3.5.4 close below it as the prior
entry. `audit` no longer emits version_drift.
references/review-triggers.md said a risk_surface fire whose resolve reports
surfaces_answered:false "does not proceed to detection until the project has
answered". Nothing implemented that. route.mjs emitted the flag, every consumer
read the fully-populated surfaces array beside it, and an unanswered project was
byte-identical to an answered one at every point after the resolve - so the rule
held only when a model happened to read step 1 attentively. Measured on a
sibling project 2026-08-19: seven blocking fires across three phases, the
question never put to the user, found only because the user asked why no scan
had happened. The ask is also unreachable anywhere else - detect-surfaces has
exactly one caller in the tree, inside the block that gate was supposed to
guard.

Detection is `risk-check run`, so the refusal goes there: it reads config
itself and returns surfaces-unanswered when no layer answered, or when a layer
did not parse. A caller naming --surfaces has already resolved the scope and is
untouched. The predicate moves to lib/surface-scan.mjs so the seam that refuses
and the resolve that reports cannot drift apart, and both judge the configured
list against route-table.json's own vocabulary - a table naming a subset is the
case that would otherwise let this seam accept what route rejected.

The answered set now also SCOPES detection. route.mjs:172 already calls the key
"the categories the one blocking trigger is scoped to"; until now risk-check ran
all eight regardless, so the answer reached the resolve and died there.

execute.md's opposite reading of the same flag stands and is explained rather
than reconciled away: the executor is being told what bar to write to, where all
eight is the safe direction, and the fire is where refusing is.
`archiveRequirements` matched an Active bullet on the bare `- **<ID>**:` form,
and this milestone opens its bullets as `- **GRD-01** (#219):`. Such a bullet
matched NOTHING: it was never removed, so its whole span would survive the
prune as orphaned prose, and its Shipped row was built with no parenthetical -
losing the requirement text and the issue pointer in one move. This is the
defect the corpus test exists to catch, and it caught it the moment phase 1
completed and that test stopped skipping.

Widen the lead line to admit `(#<digits>)` and nothing else, so the form stays
narrow enough that a `- **Note**:` prose bullet is still unmatchable, and carry
the ref into the summary rather than dropping it on the colon split - the
archived row is where a reader goes looking for it afterwards.

Two corpus repairs beside it, both of the same stale-convention family: the
test's own `activeSpan` and `parenthetical` helpers assumed that bare form too,
and this milestone's thirteen `## Active` bullets were written unwrapped
against a file whose every prior cycle wrapped at 78 columns. Rewrapping is
byte-faithful - unwrapping the result reproduces the previous text exactly -
and restores what the corpus test needs to bite at all.
requireInt took Number.isInteger, so `--total 9007199254740993` answered
{ok:true,value:9007199254740992} - a number the caller never typed, arriving
past the guard that exists to stop exactly that. It takes Number.isSafeInteger
now, a strict tightening of the same predicate.

requireCursorNumber cannot take it: its decimal form legitimately reads 2.1 and
Number.isSafeInteger(2.1) is false, so the integer predicate would refuse every
sub-phase. It bounds the parsed MAGNITUDE instead, after the CURSOR_SHAPE test
and the String(n) round trip that both stay. requirePhaseArg inherits through it
and gains no check of its own, so 1.10, 08 and 2.1 still read exactly as their
existing rows pin them.
`get` decided which requested keys were unknown with a bare `SCHEMA[k]`, so
every Object.prototype member resolved truthy through the prototype chain and
passed the filter as though the schema held it. The value line then read
`.default` off Object.prototype and assigned it into `values` at the same
name, where `__proto__` ran the object's own setter and stored nothing:
`get __proto__`, `get constructor` and `get toString` each answered
{"ok":true,"values":{}} at exit 0, and `get stakes __proto__` answered about
one key of the two asked for with nothing saying the other had gone missing.

Both index reads now carry `Object.hasOwn`, the guard `validate` already
carries at :138-142 for the same reason. No new helper and no new reason code
- `fail('unknown-key', unknown)` was always the right answer, and the fix is
that a prototype member reaches it.

The tests walk `Object.getOwnPropertyNames(Object.prototype)` rather than
hand-listing three names, so `__defineGetter__` and its siblings are covered
too, and they assert the exit status because a refusal that exits 0 is the
shape being repaired. They are kept apart from the merge-path block, which
says of itself that it pins rather than proves; both new rows go red with
either guard reverted.
normalizeNumber returned any number it was handed and Number() of any digit
string, so a row numbered 9007199254740993 read back as ...992 and a 400-digit
one as Infinity - partitionIssues then answered about an issue the tracker does
not hold, which is the whole value of "#42 is still open" gone. Both arms are
guarded by Number.isSafeInteger and return null, this function's existing "no
readable number" answer that normalizeList and readOneIssue already act on.

normalizeList still fails the WHOLE read rather than dropping the row, per its
stated design and D-11: one pathological row degrades /cad-land step 1 to its
one-line skip, the accepted cost. scanIssueRefs has no envelope to refuse into,
so an out-of-range reference is excluded from the returned array instead of
carried as Infinity to the tracker.

Tested through the exported surfaces - the three HOST_TABLE normalizers and
scanIssueRefs - rather than by widening the seam to export normalizeNumber.
`checkPairs` asked `retiredKeyError(key)` and then `SCHEMA[key]`, both bare
index reads on plain objects. `RETIRED_KEYS['__proto__']` resolved to
Object.prototype, whose `since`, `replacement` and `detail` are all undefined,
so `config.mjs check '__proto__=1'` reported `retired in v2.0.0: undefined` -
a wrong diagnostic rather than a missing one, naming a retirement that never
happened and sending the user to look for a replacement. Measured the same for
`constructor`, `toString` and `hasOwnProperty`; all four now report
`unknown key`, and `set` refuses them with nothing written.

The retirement guard sits inside `retiredKeyError` rather than at its caller,
so every caller inherits it, and it is `Object.hasOwn` rather than a check on
the spec's shape: a spec whose `replacement` is null is a legitimate row and 14
of the table's 16 ship that way. A genuinely retired key is an own property of
the frozen literal, so the guard changes nothing for it - a test row carries
that claim, settling CONTEXT's flagged assumption that the vocabulary survives.
The retired-then-schema order and the comment explaining it are untouched.

Tests walk `Object.getOwnPropertyNames(Object.prototype)` at both faces rather
than hand-listing names. All three new rows watched failing against the
reverted guard first.
parseCaptureSnippets and parseArchiveRows both built their phase field with a
bare Number() over a digits-or-decimal capture, so a CAPTURE.md tag or an
ARCHIVE.md row carrying 400 digits put Infinity into the recall corpus and one
carrying 9007199254740993 or 1.10 put a neighbouring phase's number there.

Both take the ROUND-TRIP predicate String(Number(x)) === x, which is D-07's own
rule and the one requireCursorNumber already carries, so a phase spelling has
one rule rather than two. Number.isSafeInteger would strip every legal 1.1
sub-phase, and a magnitude bound alone still admits 9007199254740990.1, which
rounds to a different phase - the exact wrong answer this closes.

They answer differently because their shapes differ. A capture tag is optional
metadata the replace callback also strips, so an out-of-range one emits no
phase AND stays in the text, the bullet reaching the corpus whole. An archive
row names a directory nothing in this tree can address, so the row is skipped -
the posture that reader already takes on a non-matching line - and the declared
phase: number return shape stays as it is.
requirePhaseArg returns the caller's raw spelling beside the numeric value, and
these two faces WRITE the numeric half - the cursor's Phase: line, and a
Traceability cell that parseRequirements and audit compare against ROADMAP
phase numbers. On a tree holding both phases/1.1 and phases/1.10, --phase 1.10
wrote "Phase: 1.1 of 2 (One)" and "| BBB-01 | Phase 1.1 | Pending |" - the
other phase's name and the other phase's row, silently, with ok:true.

One predicate in this file, called from both faces after requirePhaseArg and
before any read or write: String(value) must equal the raw spelling, so 1.10,
1.0 and 01 refuse through the existing bad-args reason while 2, 2.1 and 10
pass. The detail quotes both spellings because the caller's fix is exactly one
of two things - retype the flag, or rename the directory.

Scoped to these two faces (D-07): requirePhaseArg is unchanged, its raw field
and the rows pinning it stay, phase-done keeps reading .value so --n 02 still
boxes its phase, and plan-overlap --phase 08 still answers no-phase-dir. The
seed-reqs 08 row moves with the face it covers: the claim it makes - 08 never
becomes 8 here - now holds by refusal rather than by a missing directory.
STATED COST: neither face can name a phases/1.10/ directory any more.
The presence test read the flag's VALUE, and optionalFlag answers undefined
for a trailing valueless --date exactly as it does for an absent one, so
`bump --version 1.1.0 --date` wrote the manifest and a `## [1.1.0] - <today>`
heading and reported ok:true. That is the absent-vs-empty collapse D-05
refuses for `--date ''`, arriving by the other door. Presence is now the
flag's own appearance in argv; an absent --date still dates today.
issue-check.mjs's onPath moves to lib/on-path.mjs and is imported back, so
the seam that advises a land and the seam that will name an executor's lint
command cannot answer "is this binary reachable" differently. The module is
pure fs with no subprocess and reads no CADENCE_* variable, keeping
issue-check.mjs's promise that a PATH-prepended stub exercises the production
resolver. It gains a PATHEXT arm so npm/npx/tsc resolve on win32, and a
one-directory half for the npx-delegated probe detect-commands needs next.

A helper-census row keyed on the access probe reddens on a paste-back.
Ten lines of lib/risk-diff.mjs matched its own CONTENT_SIGNALS when the file
arrived as an added file in a diff, so a whole-file add evidenced six of the
eight categories - auth, migrations, billing, concurrency, destructive and
untrusted_input - from the table itself rather than from anything the file
does. The risk_surface gate is blocking at every stakes level, so a phase
editing the detector spent its one re-arm on a self-match.

Five label-side sites become a two-literal concatenation and five pattern-side
groups open each alternative with a one-character class. Both preserve reach
exactly: the label bytes are unchanged, so no emitted signal string moves, and
[x]yz matches the same language as xyz. Verified two ways - the 68 existing
rows pass untouched, and a line-by-line comparison of the pre-edit and
post-edit tables over 121,656 lines of tracked source diverges nowhere.

No path rule, no filename rule and no exemption list keyed on this file: the
fix is at the mention, per the discipline lib/merge-warnings.mjs states for a
lexical rule that reported itself.
parseRequirements split on an anchored heading regex and then on the next `## `; setReqStatus and insertReqRows each walked the lines with their own heading test plus a `/^## /` bound. All three now take BOTH ends from sectionSpan (D-08), so a fenced example of the heading - the shape templates/REQUIREMENTS.md ships, its whole body inside a markdown fence - is documentation rather than the section, and a fenced `## ` inside a real section can no longer end it early.

The locator is the only change: the row regex, the dashes-and-colons separator blacklist, the Pending literal, the anchor-line rule and the preserved line ending are untouched, and the fenced-only case gets the answer the absent-heading case already got - no rows, no status change, no-traceability-table with the text byte-identical.

D-14's widening rides along and is stated at each site: sectionSpan matches by trimmed equality, so a heading indented up to three spaces now begins the section.

Six of the eight new rows were watched failing against the pre-fix tree.
Nineteen lines of this file matched the detector under test when the file
arrived as an added file in a diff - ten auth lines carrying the JWT fixture,
two migrations lines carrying the column-add SQL, a comment and a context-line
fixture naming an SQL table drop, and five harness lines reading JSON. A
whole-file add evidenced four categories on a file that only describes them.

The constructs must still be there at RUNTIME or the fixtures stop testing the
detector, so the split is in the source spelling only: one shared module
fixture behind AUTH_MODULE, one shared SQL fixture behind MIGRATION_SQL, a
parseJson alias for the harness's own reads, and a two-literal join at the one
remaining context line. Every fixture's bytes are byte-identical to before -
68 tests pass with the same 60 top-level names, and the rows that assert auth,
migrations and destructive DID fire are among them.
archiveRequirements took the ## Active span from sectionSpan and then, in the same function, located ## Shipped with a bare findIndex over an anchored regex and filtered Traceability rows with a hand-rolled inTrace flag - one function, three locators, two of them fence-blind. Both now take their span from sectionSpan (D-08), and the append-after-last-row loop takes its END from the ## Shipped span rather than a fresh /^## / break (D-13).

The document that makes all three matter at once is the one this plugin ships: templates/REQUIREMENTS.md carries its whole body inside a markdown fence, so a template-seeded project's only ## Active, ## Shipped and ## Traceability headings are examples. Such a document now takes the absent-heading arm and gets a real section created rather than having rows appended inside its own code block, and its fenced table keeps every row.

What is removed and what a row renders as are unchanged: the narrow escaped-id bullet form, the whole-span capture and its whitespace join, the pipe escape at the interpolation and the section preamble are untouched. All three new rows were watched failing against the pre-phase tree.
AC7 proved where a close actually runs. The fixture is the shape a template-seeded project has: templates/REQUIREMENTS.md puts its whole body inside a markdown fence, so ## Active, ## Shipped and ## Traceability exist only as examples of themselves (D-15).

Fenced-only: ok:true, an empty moved list, created_shipped false, REQUIREMENTS.md byte-identical - which is 'no section found' in the envelope the seam already emits, so no planning.mjs edit was needed. With real sections below the fence: the archived row lands under the REAL ## Shipped after its last row, the fenced example keeps every byte, and only the fenced table still carries the example's Traceability row.

Both rows were run against the pre-phase copies of lib/planning-files.mjs and lib/milestone-prune.mjs and observed failing: on that tree the close wrote | STOR-01 | 1 | Complete | v1.2.0 | INSIDE the fenced ## Shipped table and deleted the example's Traceability row, while leaving the fenced ## Active bullet alone - the asymmetry this phase closes.
The fix in the two commits before this is a spelling discipline, and a
discipline nothing tests is undone by the next edit under a green suite. This
row reads both files off disk, synthesizes the whole-file-add diff each would
arrive as, and asserts scanDiff returns no matches under this repository's
answered surfaces and under all eight.

It asserts the category set and never a line number, so an unrelated edit to
either file cannot redden it, and it reads both files with no try/catch: a file
the guard cannot read is a failure, not a skip. Watched failing with
lib/risk-diff.mjs restored to its 0e7844b blob - 68 pass, 1 fail, the census
row naming the destructive and untrusted_input matches - and the same scan over
both pre-phase blobs returns the six and four categories the row's header
records.
A manifest is evidence of intent, not of an installed binary. A winning arm is
now offered only when its driver resolves - and, for an npx arm, the tool npx
would delegate to, which resolves on PATH or at <root>/node_modules/.bin. An
unreachable arm nulls its slot with the tool and the manifest named in
warnings[] and never falls through to a lower one, so a tree holding
[tool.ruff] and a go.mod is not told to run go vet when ruff is absent.

Reachability is pinned by the fixtures rather than by the machine:
CADENCE_DETECT_REACHABLE supplies the set in place of the probe, read by
presence (an empty value means nothing resolves) and only when
CADENCE_TEST_SEAM holds, because the value decides which command an executor
is told to run. The suite passes with ruff/mypy/eslint/tsc/go absent and again
with each stubbed onto PATH.
lib/plan-key.mjs states what a --plan value may be: the worker key seams.md
permits a dispatch to carry, so a plan number and a fix-pass key like 1-fix or
1-cut-b are both legal. It refuses a non-string (the valueless-flag rail
requireInt was standing for), an empty or whitespace-only value, outer
whitespace, and a NUL or newline - the first because rowKey joins identities
with a NUL, the second because the record and the receipt live in append-only
JSONL. An accepted key comes back verbatim: normalizing would mint the second
spelling the record/receipt join cannot survive.

plan-key.test.mjs states the table one row per spelling, and a helper-census
row reddens on a second copy of the rule.
risk-check run and risk-check status each called requireInt on --plan while
status derived what it demanded from the lifecycle brackets, where a worker key
is not always a number. A fix pass bracketed 1-fix therefore left a gate that is
blocking at every stakes level with no argv that could satisfy it and no exit
but an override. Both faces now consult requirePlanKey once each, and run
records the key exactly as the caller spelled it so the record and the fire
receipt join.

status does not narrow (D-01), with one bounded exception in the opposite
direction: a bracketed key the grammar REFUSES is reported on a malformed[]
array rather than demanded in missing[], because no run could ever record it. An
absent key is not malformed and keeps its row. The trace write face is
deliberately untouched (D-03): 239 role-keyed events would fail a numeric rule.

End-to-end rows live in plan-key.test.mjs because plan 2 holds risk-diff.test.mjs's
lease; one of them walks every row of the grammar table through BOTH faces.
Task 5 must add prose to cadence-core/workflows/execute.md while its Verify
requires self-verify to pass, and the byte-budget manifest pins that file at
26928 B, its exact pre-change size. The two are unsatisfiable together without
the manifest in the lease. User-authorized at the structural checkpoint.
The rules each seam CLI restated - what a flag's value may be, and what
happens when it is not that - get one home: lib/arg-contract.mjs declares
required-ness, a type and TWO separate dispositions per flag (value and
bare-flag), and one pure evaluator classifies a value against that
declaration.

The disposition vocabulary is exactly three words, because all three are
reasoned positions already shipped here: refuse (flagValue's rule),
warn (route.mjs's --phase, where a usage refusal would route the phase
lower than its own risk baseline) and fallback (issue-check's --timeout-ms,
whose seam may never fail a land, and the drop-on-bare spreads in the
trace append|close body). Dropping a bare flag IS fallback and gets no
fourth word.

The type names reach this tree's existing classifiers rather than
re-deriving them - requireInt, requireCursorNumber, requirePhaseArg,
requirePlanKey - plus a boolean type for the flags whose bare form is the
value. The absent/present-with-nothing-usable split is read off flagValue
itself, consulted rather than re-spelled, so the helper census still finds
one home for it.

One flat {ok, value, detail} on every path, never a JSDoc discriminated
union: the CI typecheck runs strict:false, where narrowing by a boolean
literal does not happen. ok:false refuses, ok:true with a non-empty detail
warns, ok:true with an empty one accepted or fell back.

It emits nothing, reads no process or environment and touches no
filesystem - the caller owns its envelope and its reason string - and it
governs VALUE grammar only: flag membership stays self-verify check 2's
prose-side job.
The subcommand/flag table was defined in self-verify.mjs, beside the prose
lint that consumes it. It is defined in lib/arg-contract.mjs now, beside
the evaluator the seam CLIs refuse with, and self-verify.mjs imports it:
one source, not two bound by a check (D-06). Two tables is the drift
ARG-06 exists to end reintroduced by the fix - a flag added to one and not
the other is either silently accepted at the CLI or reported unknown-flag
against correct prose.

Every row moved with its comments: the '*' global row, the '' bare-form
row, all 16 script keys and all 77 subcommand rows, no flag name changed.

Check 2 stops spreading a row directly and asks flagNames() for its flag
names instead. That accessor is what keeps the prose lint working once a
row carries a value grammar rather than a bare array; without it the lint
would read a row's flag SPECS as flag names the moment the grammar lands.
TWO_WORD stays here - check 2 is its only reader.

The header prose that named a location this file no longer has is
corrected in the same edit: check 2's entry, check 14's entry, and the
check-14 fixture comment that said the table lives in self-verify.mjs
itself. A new test pins the single table - self-verify.mjs defining a
CONTRACTS of its own again reddens, and every top-level bin script is
asserted to have a row in the imported table.
All 144 flag entries across 16 scripts and 77 subcommand rows stop being
bare names and declare what their value may be: required-ness, a type, a
value disposition and a separate bare-flag disposition. flagNames() is
what keeps the prose lint reading names off rows that now carry four
fields each.

Required-ness is per SUBCOMMAND, never per flag. risk-check run requires
--base and --head; risk-check status takes the same pair optionally,
because its triple is all-three-or-none and that rule is the seam's.
--plan is three different types on three rows for the same reason -
lease-check names a plan FILE and stays int, risk-check names the worker
key and reads plan-key, trace append stores the caller's string verbatim.

The dispositions reproduce what ships rather than tidying it. --dir and
--root refuse the empty, bare and flag-shaped spellings. --branch, --base,
--remote, --merged and --version declare fallback on the bare form, which
is what their seams' || fallback absorbs today. --timeout-ms falls back on
a malformed VALUE as well, because issue-check may never fail a land.
route.mjs's --phase warns on both axes, never refuses, which would route
the phase lower than its own risk baseline. On trace append|close, --plan,
--sha, --base and --detail keep the drop while --step, --reviewer,
--trigger and --role refuse the bare form - the split that is the whole
reason the two axes are two fields. --date refuses the bare form, the rule
release-bump hand-writes today beside the permissive reader.

A boolean row's two dispositions are inert by construction and are still
declared, because an omitted field is the silent opt-out the completeness
test exists to catch. That test walks every script, subcommand and flag and
reddens on a missing field, an unknown word or a fifth key; a second test
pins the declarations the CONTEXT decisions bind, so a reversal cannot
land quietly.
The tracer bullet for the whole contract - declaration to evaluator to CLI
refusal to envelope - and the first adopter is deliberately the file the
table just left. The entry block stops calling flagValue by hand and asks
the evaluator to apply CONTRACTS['self-verify.mjs']['*']['--root'], so the
rule comes from the declaration rather than from a call this file restates.

The throwing mechanism and the e.seam catch arm below it stay (D-08): the
refusal still emits one stdout line
{"ok":false,"reason":"missing-flag-value","detail":"--root"} with exit 1
and nothing on stderr, and without that arm a thrown seam object carrying
no message would surface as detail "[object Object]". The reason string is
this file's, named at the throw and held in a const - the contract mints
none of its own (D-07), and the literal throw body is pinned to
lib/seam-input.mjs by the helper census.

A genuinely absent --root still falls through to the plugin's own tree.
The empty, valueless and flag-shaped spellings refuse as before, and a
whitespace-only one now refuses too rather than linting a tree that is not
there. The 25 checks are untouched.

The existing refusal test gains the three assertions the criterion names -
exit 1, zero bytes of stderr, exactly one stdout line - and pins the row
itself, so loosening the declaration reddens rather than quietly loosening
the CLI.
The global --dir now reads through its declared row in lib/arg-contract.mjs
instead of off parseArgs' opts. Measured before the fix: `status --dir ''`
answered ok:true about ./.planning, a tree the caller never named, and a bare
--dir minted the boolean true, reached existsSync(true) and printed a DEP0187
deprecation warning on stderr beside the answer - stdout is the single channel
the seam layer parses, and that deprecation is scheduled to become a throw.

The refusal keeps this file's ONE vocabulary, fail('bad-args', ...), never the
missing-flag-value throw: there is no e.seam catch arm here to render it as
anything but internal. A genuinely absent --dir still defaults to .planning.
The shared `trace append|close` body ran both bare-flag dispositions as seven
hand-written guards. It now reads all seven string flags through their declared
rows in lib/arg-contract.mjs: --plan, --sha and --base declare `fallback` and
keep reading as absent, while --step, --reviewer, --trigger and --role refuse.

--role MOVED to refuse, and that is the behaviour change. A bare --role wrote a
complete-looking dispatch carrying a token figure and no `role` key, and
`trace render` then aggregated it under the empty-string key, so the
attribution the flag exists for was gone. trace.test.mjs's row pinning the
drop is rewritten to pin the refusal, beside a new row pinning the fallback
half so the two dispositions cannot collapse into each other.
`parseArgs` read every value as `a[++i]` with no flag-shape test, so a
valueless flag ate the flag after it: `resolve --role --attempt 2` returned
`unknown-role: "--attempt"` with the attempt silently reverted to 1, a refusal
about a role the caller never named.

The five value-carrying flags of `resolve` now read through their declared rows
in lib/arg-contract.mjs, so the missing, empty and flag-shaped spellings are one
refusal naming the flag whose value went missing. The reason stays route.mjs's
own `usage` and the published detail wording is unchanged; this bin mints no
reason code of its own. An absent --role still prints the whole synopsis, and
--phase stays RAW - it declares the warn disposition and a usage refusal there
would route the phase lower than its own baseline.
`--phase` declares the warn disposition, and it produced nothing:
`requirePhaseArg` sat inside the trace-phase derivation's try/catch and its
`!parsed.ok` arm fell silently through to the cursor, so `resolve --role
cad-planner --phase 1.10.3` returned ok:true with no mention of --phase and the
routing event keyed to a phase the caller never named. The comment there said
the shape check belonged where the risk floor was computed; that floor is
retired, so nothing carried it.

The flag now reads through its declared row and the diagnostic rides
`warnings[]`, which rides every result shape including ok:false. The resolution
is untouched: a malformed --phase still falls back to the cursor and returns the
same bundle it returns with no --phase at all. route.mjs stops calling
requirePhaseArg directly - the row names `phase` as the type and the table maps
the type to that one classifier.
`parseArgs` did `opts[a.slice(2)] = rest[i + 1]` with no flag-shape test, so a
valueless flag ate the flag after it and the value after THAT was skipped:
`consult --payload --provider openai` returned `bad-provider: "unknown
provider: undefined"`, a refusal about a flag the caller did pass, naming the
wrong problem.

The six declared flags now read through their rows in lib/arg-contract.mjs, so
the missing, empty and flag-shaped spellings are one refusal naming the flag.
The reason is `bad-args`, already published in this bin's degradation list;
references/seams.md is untouched. The value door only - an absent flag still
reaches the handler that owns its wording, so an unknown provider is still
bad-provider. parseArgs stays pure and keeps its {cmd, opts} shape: the refusal
rides back as a third field and main renders it.
`optFile` hand-wrote the value rule as `if (!tokens[i + 1])`, which caught the
two spellings an interpolated `--file $VAR` produces but read a FLAG-SHAPED
token as a path: `config.mjs validate --file --nonsense` returned
`{"ok":false,"reason":"read","detail":"cannot read/parse --nonsense: ENOENT"}`,
answering about a file the caller never named.

All three spellings are one rule now, read through the per-subcommand row in
lib/arg-contract.mjs. The refusal keeps this bin's own `usage` and its published
wording verbatim, and both locked mechanics stand: --global is tested first and
short-circuits before --file is looked at, and the consumed flag and its value
are still filtered out of the token list set and get read keys from.
git-branch.mjs, git-publish.mjs, land-cleanup.mjs and worktree-base.mjs stop
stating their own flag rules. Each reads every flag through CONTRACTS's
declared row via requireFlag - evaluateFlag's classification RAISED as
lib/seam-input.mjs's refusal object, which is the throwing half D-08 keeps for
the bins that already hold an e.seam catch arm. It mints no reason code (D-07):
missing-flag-value is the code these four already publish.

--dir declares refuse on both axes, so the empty, valueless and flag-shaped
spellings still refuse on one stdout line with exit 1 and nothing on stderr,
and an ABSENT --dir still falls through to process.cwd(). --branch, --base,
--remote and --merged declare fallback (D-12), so a spelling carrying no usable
value reads as absent and each seam's own default answers as it always did.

Two spellings change as a consequence of fallback meaning "reads as absent":
git-branch.mjs decide --branch --dir <p> no longer swallows --dir as the branch
name (the D-13 defect) and derives the branch instead, and reap --branch
--force now refuses no-branch rather than bad-branch - the flag-shaped token is
stopped one layer earlier, before decideReap sees it. The safety property is
unmoved: ok:false, exit 1, no argv built, the branch still there. SAFE_BRANCH
keeps its own test against decideReap directly in publish-decision.test.mjs.

helper-census.test.mjs matches the refusal object at its CONSTRUCTION rather
than at the throw, since two files now throw it and exactly one builds it.
Both stop stating their own flag rules and read every flag through CONTRACTS's
declared row via requireFlag, keeping their own e.seam catch arm.

issue-check.mjs's --timeout-ms declares the int type with the fallback
disposition on both axes, so a malformed, empty or valueless one still reads as
absent and DEFAULT_TIMEOUT_MS still answers - this seam's whole contract is that
it never fails a land, and a contract that made every typed flag refuse would
hand it the power to fail one (D-04). Its own extra term stays spelled at the
call site, since the shared int type does not carry positivity. --base declares
fallback and --dir declares refuse, so --dir still refuses the empty, valueless
and flag-shaped spellings before any spawn.

weight.mjs keeps the throwing form for --root, --command and --role, all three
declaring refuse on both axes - the header's "never a silent default", now read
off a row. Its unknown-command and unknown-role throws stay weight.mjs's own
domain vocabulary: the flag was well-formed and the seam is saying the filter
matched nothing, which is not an argument-shape refusal.

One spelling changes, the D-13 flag-swallow closing as it did on the four --dir
seams: issue-check check --base --dir <p> no longer swallows --dir as the base
branch and fails the log read, and now answers exactly as the same call without
--base does. A 20-row before/after matrix over both bins is otherwise
byte-identical, including every --timeout-ms fallback and every --dir refusal.
--date is the flag that made the bare-versus-empty split visible in this tree:
the dispatch read the value with the non-throwing reader and then probed the
flag's OWN appearance in argv beside it, because '' is falsy and the permissive
reader answered undefined for a TRAILING valueless --date exactly as it did for
an absent one. Its declared row refuses on BOTH axes, so that rule now comes
from the declaration and the second probe is gone.

--date is read in the RETURNING form and --dir and --version in the raising one
(D-08, picked per flag here because this seam publishes two vocabularies).
Everything the date refusal says stays: badDateDetail's two sentences, the
bad-date reason, and the envelope carrying no manifest, siblings or changelog
fields, because those are filled from a manifest this path never reads.

Measured before/after over 18 spellings, three change and the rest are
byte-identical. Two are the D-13 flag-swallow closing: bump --version --dir <p>
and bump --version --date <d> no longer read the neighbouring flag as a target
version and answer no-target-version instead of unparseable-version. The third
is --version "   ", which the row's string type refuses as missing-flag-value
where the blank previously reached normalizeTargetVersion and came back
no-target-version - both refuse and write nothing, and the header's
missing-flag-value line now names --version alongside --dir.

The header's stale residue note is corrected with it: it claimed a bare
trailing --date still dates today, which the presence probe had already stopped
being true.
With the last bin caller gone, lib/seam-input.mjs stops exporting the permissive
positional flag reader. A flag that legitimately defaults now DECLARES the
fallback disposition on its row, which is the same "reads as absent" answer
reached from a declaration rather than from a second reader - and without the
defect the positional reader carried, which returned the NEXT FLAG as a value.

flagValue and readText stay exported and unchanged: D-08 keeps the throwing
mechanism alive for the bins that hold an e.seam arm, and only the permissive
reader collapses.

Four surfaces move in the same commit, the discipline phase 2's D-01 set when it
edited the header rather than leaving it to go red:

- lib/seam-input.mjs's header stops opening "THE TWO FLAG READERS ARE TWO
  CONTRACTS, AND BOTH ARE LIVE" and states the collapse as the second reversal
  of that guarantee, after phase 2 reversed it for --dir.
- seam-input.test.mjs loses the divergence arm and the four permissive-reader
  arms: there is no second answer left to disagree with, and what a defaulting
  flag does with nothing after it is arg-contract.test.mjs's question now, asked
  of the declaration. One arm replaces them, pinning missingFlagValue's two
  fields at the one construction two files raise.
- helper-census.test.mjs's row for that reader moves to lib/arg-contract.mjs and
  matches the fallback arm's body, so the disposition keeps exactly one home.
  Watched failing first: a probe module carrying the arm turned the census red
  naming lib/zz-census-probe.mjs, green again on removal. Every pattern is still
  built from an escaped string, so this file is censused by the same walk with
  no exemption, and lib/arg-contract.mjs joins the walk's named-file list.
- lib/arg-contract.mjs's dispose() records where the reader ended up and what
  the moved census row can and cannot catch.
A tenth seam's author now reads what a flag must declare instead of
inferring it from nine existing bins. The new Seam arguments section
states the three dispositions (refuse, warn, fall back) with the shipped
case behind each, states the bare-flag axis as a field separate from the
value axis, and names cadence-core/bin/lib/arg-contract.mjs as the one
place a new flag is declared. It also states why one --dir rule surfaces
as bad-args in one bin and missing-flag-value in another: the contract
classifies, the caller owns its reason string.

conventions.md sat at exactly 0 B of headroom, so its weight-budgets.json
row is re-pinned to the measured 14556 B in the same commit.
`lib/arg-contract.mjs` gains the two things an adopting dispatch needs:
`subcommandKey`, a MOVE of self-verify.mjs's `TWO_WORD` set and its
resolution expression, and `evaluateRow`, which applies a resolved row's
value grammar to one argument list and returns the first refusal.

The door is a VALUE door: it judges only the flags actually PRESENT and
leaves an absent-but-required flag to the bin that owns the wording, which
is review-provider.mjs's shipped position. It evaluates the `'*'` row before
the subcommand's own, because a script-global flag is what answers first
today, and it carries no wording and no reason code (D-07).

self-verify.mjs check 2 reads the resolver back, so one rule has one
spelling rather than two that can disagree.
The 98 rows this script declares were consulted at two sites, so a row could
say `refuse` while the CLI wrote the value through: `cursor set --name`
(bare) answered ok:true and wrote `Phase: 1 of 5 (true)` into STATE.md, and
a bare `--sources` and `--reason` wrote `true` into a UAT checklist. The
dispatch now runs every flag the resolved row and the `'*'` row declare -
and that is present in ARGV - through the row door before any handler runs.

Absence does not move: an absent flag is left to the handler that owns its
wording, so `cursor set needs --phase <N>`, `capture --kind must be one of
todo | seed | note` and `bad-result` all answer exactly as they did.

The refusal sentence comes from one flag->sentence map, composed with the
subcommand prefix a flag's row implies - none for the `'*'` row, the
subcommand for its own - which reproduces the `--dir`, `--root` and
`--<field>-file` lines character for character. The four hand-written
`--root` guards the door makes dead are deleted; `renumber`'s decimal
diagnostic moved to the door rather than being lost, since a declaration
cannot state a rule about the value.

`--tokens` declares `fallback` on the VALUE axis on both trace rows: its
body grammar is wider than `int` can state (a comma-grouped `146,405` is the
form this plugin prints figures in) and refusing it dropped an append whose
`dispatch` half was already written.
The door refuses four flags the shared `trace append|close` body also
refuses, so `planning.mjs` held each of those sentences twice.
`TRACE_REFUSALS` folds into the single map beside the dispatch and the trace
loop composes its refusal through the same helper, so `trace append --role`
(bare) answers with the same sentence whichever side refuses it first.

`TRACE_GRAMMAR` and the string-flag loop stay: the grammar UNIONS the append
and close rows because one body validates both, while the door evaluates the
resolved subcommand's row alone - and `trace close` declares no `--sha`,
`--base`, `--step` or `--trigger`, since a flag row is a prose allowlist that
never widens what a subcommand accepts. Cut the loop and a bare `--sha` on
`trace close` starts being written into the event as the literal `true`.

A census in trace.test.mjs pins each of the four sentences to one home.
arg-contract.test.mjs asks whether the table is well formed. This asks the
only other question that matters: does the shipped CLI do what the row says.
The gap between the two is what let 96 planning.mjs rows state refusals the
bin never applied.

The walk is total. 231 declared refusals are exercised by spawning the owning
script with that subcommand's words and the flag as the last token (bare
axis) or with a malformed value chosen by the declared type (value axis);
each must exit 1 with one JSON line that is ok:false and names the flag. 33
fallback/warn axes and 12 boolean entries are counted as skips with their
reasons, and the counts are asserted, so an entry that stops being exercised
reddens rather than passing vacuously.

Watched failing first: reverting the door to its pre-adoption reach reports
116 unapplied refusals, `planning.mjs cursor set --name` among them.
Loosening a declaration is the other direction that could re-open the defect
and the census SKIPS it, so the three rows UAT item 8 reproduced are pinned
in arg-contract.test.mjs as well - watched failing on `--name`.

No live fallback or warn arm: proving either means the command RUNS, and
git-publish publish and milestone-prune are mutations.
`config.mjs get stakes --global` answered ok:true while
`CONTRACTS['config.mjs'].get` declared only `--file` and its two siblings
each declared `--global`. Self-verify was green solely because no workflow
prose spells that pair, so check 2 never reached it - correct prose spelling
it would be reported `unknown-flag` against a flag the seam accepts.

The `get` row now declares `--global` with the grammar `validate` and `set`
carry, and `optFile` reads it through `evaluateFlag` against
`CONTRACTS['config.mjs'][cmd]` rather than through a hand-written
`tokens.indexOf('--global')` probe. Reading it off the row is what makes the
class unrepeatable here: a subcommand accepting a `--global` it does not
declare is no longer expressible, because the read needs the row.

Both mechanics stay: `--global` is tested FIRST and short-circuits before
`--file` is looked at, and the returned tokens still have the consumed flag
filtered out. Watched failing: deleting the row stops `get --global`
answering `source: "global"`. The table's entry pin moves 144 -> 145.
Both surfaces claimed the rules were declared once and enforced, and UAT item
8 was raised against exactly that claim. Three sentences make it true where
it now is and narrow it where it is not.

The DECLARATION is what refuses: an adopting bin runs its resolved row at the
door of its dispatch rather than restating the rule at each handler, and the
adoption census walks the table against the shipped binaries, so a row
declaring a refusal nothing carries out reddens. The bound that census cannot
reach is stated with it.

PRESENCE is the exception, and this is where review-provider.mjs's shipped
carve-out is written down: a genuinely absent flag is answered by the bin
that owns the wording, because a missing enum-valued flag's diagnostic is not
expressible in a declaration.

And the boundary that already existed stays stated - value grammar only,
never a runtime refusal of an undeclared flag - now with the corollary that
the module owns no wording either, which is what leaves a bin free to keep a
diagnostic no row can state.

`conventions.md`'s weight-budgets.json row moves 14556 -> 15791 in the same
commit, since that file sits at exactly 0 B of headroom.
A row declaring `refuse` was bypassable by typing the flag twice. `evaluateFlag`
read the value through `flagValue`, an `indexOf` reader that answers about the
FIRST spelling, while the bins that consume the same argv do not agree with it:
`planning.mjs`'s `parseArgs` keeps the LAST. So `cursor set --phase 1 --total 5
--status planned --next /x --name valid --name` passed the door on `valid` and
then wrote boolean `true` into STATE.md as the phase name - the exact corruption
plan 5 states the declaration exists to refuse, at a spelling the census only
ever exercised as a sole occurrence.

Every occurrence is judged now. The refusing one wins wherever it sits, a
warning one wins over a clean one for the same reason, and only when nothing
else fired does the first occurrence's accepted value stand. The bare/value
split is untouched and `flagValue` is still consulted rather than re-spelled:
the tail is sliced at each occurrence so the shipped rule answers per position.

`decimalRefusal` gains the decimal test it had only implied. `requirePhaseArg`
accepts a plain integer, so the renumber wording fired on a well-formed `--at 1`
the moment a later occurrence became the refusing one, telling a caller to
re-place a decimal they never typed; and the wording now reads whichever
occurrence carries the decimal rather than the first.

The adoption census walks its whole refusal table a second time with a
well-formed occurrence of the same flag ahead of the malformed one, so the
declaration is proven under duplication for all 231 refusals rather than for the
one case that was reported.
Plan 5 and the risk_surface fix pass folded in: the commit table gains seven
rows, the goal check is rewritten around what the adoption census now proves
against the shipped binaries, and the open items carry the four the phase leaves
behind. The `config.mjs get --global` item is struck rather than restated - plan
5 closed it.
Five empty returns and one medium on arg-contract.mjs:284, where a repeated
flag's later fallback occurrence loses to an earlier clean parse. Tracked
because every other phase's review record is.
Every config key, script invocation and repo path that moves out of
README.md into docs/ stops being CI-enforced the moment it moves - the
two unchecked weight.mjs invocations in docs/EVIDENCE.md were the live
precedent. This lands before any prose moves, so the relocation is
written under the lint rather than checked by it afterwards.

The new test asserts the reported file path, not just the problem kind,
so an edit that drops the directory from the walk reddens instead of
going quiet.
The landing page asks one decision, whether to install this. What a run
costs on my own account is the material a reader wants after that
decision, not before it, so it moves to its own page and README keeps a
pointer beside the ones for METHOD, INTERNALS, WORKFLOW and EVIDENCE.

Re-wrapped to 80 columns rather than moved byte-for-byte, so the diff
shows which words the accuracy pass changed. The one measurably-false
figure goes with it: the skill and agent descriptions did get cut to one
routing line each, but "8,550 bytes to 5,397" reads 6,034 across 52
frontmatter blocks today and carries no historical frame the way the
turn-one figures beside it do. The claim stays, the stale number is
gone, and `weight.mjs resident` is what prints the current one.
A reader deciding whether to install this does not need a phase-by-phase
walkthrough of a project they have not started. It moves to its own page,
re-wrapped to 80 columns with the four-command fence left alone, and
README keeps a pointer to it.

Every claim was re-read against the tree rather than carried across. The
sentence naming /cad-suggest's retune output and git.issue_check still
holds: the schema's git.issue_check purpose says the report only reads,
cad-land's step 1 says landing closes no issue, and its publish step
still asks with no preselected default.
Twenty-one bullets in argument voice, deliberately omitting the six
build-spine commands The loop already covers, while
cadence-core/references/COMMANDS.md publishes all 27 in reference voice
where a browser can read them. It is material you look things up in
rather than the argument for why the gates exist, so it fails the staying
test and does not belong on a docs/ page either. One line at the end of
The loop points at both references instead.

Three arguments were worth keeping and are folded in as prose: cad-audit
catching silently-dropped work, cad-minimalism-review's ranked
delete-list that it applies none of, and cad-report pricing every
subagent - the last one matters more now that the cost section has left
the page.
The old order asked for the install decision and disclosed the price
underneath it. This reverses that: the section naming the moments the run
stops - before it plans, at every check it runs, and before it pushes
anything - now sits between the opening paragraph and Install.

It states the demand and never a label, because a label invites a reader
to self-identify into the flattering box while a demand makes them do the
arithmetic about their own habits. It states no count either: the fifteen
decision points are mostly system-answered, so the figure overstates the
felt interruption while omitting the push stop, and any count of the
unconditional stops drifts as they move.

Line 1 through the end of Install is now free of gate, seam, rung,
dispatch, adversarial, traceability and subagent. Cadence's own
vocabulary resumes at The loop, where a reader has already bought in.
The loop moves above How it works, so the page reads what you run before
why it is shaped that way: opening, the demand, Install, The loop, How it
works, What a break costs, Where it came from.

How it works goes from 5,914 bytes at the start of this phase to 4,465
without losing an argument. The two war stories compress into the rule
they teach rather than being deleted - do not try to out-parse an
attacker, delete the thing you would have had to parse - since that is
the strongest case on the page for why the rails are shaped the way they
are. The four load-bearing claims stay: durable state, a check that could
not run never passing, the hook rather than a paragraph, and the reviewer
posture where every backend returns the same shape.

What a break costs and Where it came from are untouched. README.md is
14,264 bytes, down from 24,850.
README's Where it came from points at this file as the one that publishes
the counts, while the distance table still read 7 agents and 22 skills
against README's "27 skills and 6 agent roles across 19 rung files".
Shipping a landing page that points at a file contradicting the sentence
above the pointer is not acceptable.

Both cells re-counted by the method the Provenance paragraph names,
find/wc over agents/ and skills/: 19 rung files materializing 6 roles,
and 33 skills of which 27 are user-invocable and 6 are preloaded contract
skills carrying user-invocable: false. Each cell states its decomposition
so the row agrees with README rather than reading as a third figure, and
Retained is recomputed from the new numerators. No other row moved, and
Provenance now says which two cells carry a later date.

Nothing in CI catches a mistake here: self-verify excludes LINEAGE.md as
a historical doc and prose-agreement's count test is scoped to README.
These were read against the tree by hand.
The ledger joins on doc plus claim text, so moving two sections without
moving their rows would make the next sweep report a wave of vanished
claims and a wave of new extractions instead of a diff.

Eight rows follow the worked example to docs/EXAMPLE.md and eight follow
the cost-to-run section to docs/COST.md, claim text untouched. Nine
retire because their sentence was cut rather than moved: the six
command-list rows, the 8,550-to-5,397 clause, and two the How it works
compression took with it. README-38 and README-75 each straddled the cut
and are judged individually - README-75 loses the half saying the tag is
cut by /cad-land, so its claim text is rewritten rather than re-pinned
silently.

Every surviving pin is re-derived from the post-change file rather than
shifted: c99b778's two-line badge insert made rows stale by two only if
they predate it, so a blanket shift would have broken the rows that were
already right. Three claims the compression would have dropped are
restored to README.md instead - the four-line cursor, v2.2.0 as what
deleted the 2,251 lines, and the multi-select triage prompt - because the
sentence carrying each survived and only a detail inside it did not.
The register reframe in 47d7214 dissolved `## What it asks of you` into the
opening paragraphs and dropped the sentence the roadmap called load-bearing,
breaking two of AC2's three clauses. Land the sentence back in the opening,
where the demand now lives, and revise the rule it broke: ROADMAP and RME-01
asked for a separate audience SECTION, and the review that produced 47d7214
rejected that framing. Placement and the sentence still bind; the section
shape does not.

No line was added to README.md, so every `README-*` pin in the claim ledger
still resolves.
chore: pin executor, plan-checker and verifier effort at high
All checks were successful
test / node-test (git, 22) (pull_request) Successful in 53s
test / node-test (git, 24) (pull_request) Successful in 35s
test / node-test (other, 22) (pull_request) Successful in 58s
test / node-test (other, 24) (pull_request) Successful in 55s
test / node-test (planning, 22) (pull_request) Successful in 1m14s
test / node-test (planning, 24) (pull_request) Successful in 1m16s
test / node-test (prose, 22) (pull_request) Successful in 32s
test / node-test (prose, 24) (pull_request) Successful in 32s
test / node-test (review, 22) (pull_request) Successful in 18s
test / node-test (review, 24) (pull_request) Successful in 17s
test / node-test (routing, 22) (pull_request) Successful in 33s
test / node-test (routing, 24) (pull_request) Successful in 30s
test / self-verify (pull_request) Successful in 15s
test / typecheck (pull_request) Successful in 18s
95f28cc9b8
john merged commit ea8488c671 into main 2026-08-19 20:39:48 +00:00
Sign in to join this conversation.
No description provided.