Skip to content

pm(tooling): move the contract-review tier off the retired ceiling, and give the fuse the case it never had - #19573

Merged
os-steve merged 3 commits into
mainfrom
claude/issue-19544-contract-review-tier-off-fable
Sep 21, 2026
Merged

os-steve merged 3 commits into
mainfrom
claude/issue-19544-contract-review-tier-off-fable

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #19544

Clause-②: no

Fable is gone. The contract-review tier moves to the opus tier the harness actually
serves, and the downgrade fuse gains the case it never had: a tier that is GONE is not
a tier that is EXHAUSTED, and the difference is who may act.

The four parts

1 — the constant. CONTRACT_REVIEW_TIER in scripts/pm/dispatch-gates.mjs now holds
the opus model id. ⛔ Not recalled and ⛔ not invented: read off the harness itself — the
claude-code-remote get_session tool reports it for this session as
external_metadata.last_served_model, and session_context.model and configured_model
agree with it. The id is still spelled as a VALUE on exactly one line in the tree, which
the battery proves rather than promises (a landed case scans both roots and reds on a
second site — exercised below).

2 — the ladder. 「ceiling fable」 was a WORD written beside the constant, and that is
exactly how the pair drifted: the harness stopped serving the tier, the constant kept
naming it, and the ladder went on printing its family word as a live rule. The ceiling is
DERIVED now — tierWordOf() reads the family out of the model id, TIER_CEILING is that
derivation — so --tier renders

  The tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling opus).

and the next retirement moves ONE line. The rendered EXITS text and its docblock stop
naming the retired tier too: the quota exemption now reads "the mandated tier EXHAUSTED ⇒
opus, never lower — a tier that is RETIRED is a maintainer ruling instead, ⛔ never a
seat's reading". tierWordOf hands an id it cannot read back VERBATIM rather than
throwing: --tier runs on every dispatch, and a ladder printing the whole id is louder
than one printing a family word nobody ruled.

3 — the case the rule never had. .claude/skills/pm-dispatch/references/contract-review.md
line 60 now reads:

- 额度耗尽豁免只及派发 ⛔ 不及复核;档位退役 ≠ 耗尽,恒维护者裁决 ⛔ 非席位读数。

Paid IN PLACE — ⛔ no ceiling raised, ⛔ no ruled clause deleted. The file is still 60
lines against a ceiling of 60 at headroom 0
, and the line is 115 bytes against the
120-byte cap. The old line's two clauses (「豁免对象是派发」 and 「复核 ⛔ 不随派发档位免除」)
survive compressed into 「只及派发 ⛔ 不及复核」; 额度耗尽's existing treatment is untouched,
because it was never on this line — 「队列外等档」 is line 53's and 「⛔ 不自审」 is line 26's,
and neither moved.

4 — the pins. Ten new self-test cases hold the constant and the rule text together: the
ladder line is compared against the constant (not against a remembered word), a
retired-spelling guard (RETIRED_TIER_WORDS) is asserted over every live rendering, the
rulebook root is asserted to spell no model id at all, and the fuse's two arms are read out
of the skill file itself.

What deliberately did NOT change

The issue is explicit that a record of what was served AT THE TIME stays true, so every
occurrence naming the retired tier as history was left alone and judged one by one —
verbatim maintainer rulings in the mandate docblocks and the skills/** glob's why row,
the #8640 incident note in the battery, and the test LABELS that call the mandate
"fable-mandatory" (they already assert against CONTRACT_REVIEW_TIER, so they are
tier-agnostic in behaviour). The retired-word guard therefore covers the RULE lines and
excludes the - path ⇢ glob — why provenance rows, with a non-vacuity case proving the
filtered text still carries rules to search.

check-clause2-carriers --template's Served-tier: contract is unchanged: the key
still admits the constant's NAME, ⛔ never a model id. It was already pinned (the battery
refuses a family prefix, a suffixed constant, and the constant's own VALUE, and its refusal
never quotes an identifier back), so nothing was added there — only re-run.

Governance — Tier S, ALL-not-ANY

node scripts/pm/check-governed-merges.mjs --test scripts/pm/dispatch-gates.mjs \
  .claude/skills/pm-dispatch/references/contract-review.md

governed-surface predicate: 1 of 2 path(s) hit the register (6 surfaces, repo-agnostic).
⛔ GOVERNED — Tier S(席内达档复核落地) … .claude/** ×1 —
.claude/skills/pm-dispatch/references/contract-review.md;
paths not on the register: scripts/pm/dispatch-gates.mjs

No Tier H path is touched: AGENTS.md, CLAUDE.md, docs/adr/**, docs/NORTH-STAR.md
and the published skills/ root are all outside this diff. This PR lands on the owning
seat's at-tier review of record; ⛔ no maintainer click is waited for and ⛔ no seat
approves it.

Evidence

node scripts/pm/dispatch-gates.mjs --self-test — ✓ dispatch-gates self-test: 1893 cases pass., exit 0, zero reds, run to completion on the
final commit (the battery re-spawns the tool's CLI many times, so it is run detached per its
own header and the log read at the end).

node scripts/pm/check-clause2-carriers.mjs --self-test — 1115 cases pass, exit 0
(it imports the constant, so this change moves its verdict even though the path derivation
does not name it; CI runs it in Lint & Repo Gates).

Derived gate families — node scripts/pm/dispatch-gates.mjs --commands derived 37
commands off the merge base; all were run and all are green, with one NOT MEASURED:
check:doc-formula-expressions exits 3 PREREQUISITE NOT MET (two workspace packages are
unbuilt in this worktree) — ⛔ read as not measured, never as a pass. check:pm-skill-ratchet
and its self-test are green (157 cases), as are check:pm-governed-merges,
check:skill-frame-sync, check:watch-hint-literal, check:nul-bytes and
check:pm-skill-id-lint.

Lint, narrowed and declared. eslint scripts/pm/dispatch-gates.mjs --no-inline-config
→ 1 file, 0 errors, 0 warnings. The narrowing is measured, not assumed: ① eslint's own
population is **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}, so the .md file is outside it
entirely; ② the file count is read from --format json (1); ③ eslint.config.mjs enables
no type-aware linting anywhere (no parserOptions.project, no projectService), so this
diff cannot move the verdict on any file it does not touch. The repo-wide pnpm lint sweep
is CI's run.

Control bytes. grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' over both changed files
prints nothing (exit 1); the control — the same -P class engine asked for printable ASCII
on the same files — fires on 28,205 and 53 lines.

The retired id is gone from every live rule, with a control

The pattern is spelled as the id's SHAPE so this body carries no model identifier:

git grep -nE 'claude-[a-z]+-5-1' HEAD -- .
  scripts/check-commit-card-trailers.mjs:848

git grep -nE 'claude-[a-z]+-5-1' origin/main -- .     # CONTROL: same pattern, base tree
  scripts/check-commit-card-trailers.mjs:848
  scripts/pm/dispatch-gates.mjs:12124

The control proves the grep fires: on origin/main it finds the constant's old
declaration; on this branch that line is gone. The one survivor is a rejection
vocabulary
— the list of ids check-commit-card-trailers must refuse in a commit trailer
— not a tier rule, and deleting the retired entry would weaken the gate for old history.

Ablation — the pins are load-bearing

Each mutation was landed with scripts/ablation-replace.mjs (anchor count checked, blob
hash before/after recorded) and restored with git checkout HEAD --, proven by
blob == HEAD blob and an empty git diff HEAD.

A — move the code side without the rule. The ladder's ceiling ${TIER_CEILING}) was
replaced by the retired family word (anchor 1 → 0, blob e3a804cfc19f → d19c861ac7ee):

✗ the ladder prints a ceiling DERIVED from the contract-review constant, so the two cannot drift apart
✗ ⛔ no RETIRED tier word survives in any live RULE … (dirty: 0/fable 3/fable)

2 reds out of 1,506 cases decided; every other new pin stayed green, which is the point —
they bind different halves.

B — a second spelling of the tier's VALUE (a fixture argument in the battery rewritten
to the constant's own value; anchor 1 → 0, blob e3a804cfc19f → 517886d4a0f9):

✗ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm
  — 25+30 files read … (found: scripts/pm/dispatch-gates.mjs:12142, scripts/pm/dispatch-gates.mjs:25317)

This run is why two of the cases first written here were deleted before review: the
promise was already pinned, and better, by a landed case that derives its roots from the
mandate globs and finds the definition line instead of remembering it. One assertion, one
owner.

C — move the rule side without the code. Line 60 of the rulebook was reverted to its
pre-card wording with a model id appended (anchor 1 → 0, blob aebe3ca8b11d →
906f7ea1e9d5):

✓ the ladder prints a ceiling DERIVED from the contract-review constant …
✓ …in the ladder's own vocabulary — the constant's FAMILY word …
✗ .claude/skills/pm-dispatch spells NO model id at all — 25 file(s) read … (found: …/contract-review.md:60)
✗ …and its fuse keeps the quota exemption pointed at DISPATCH, never at the review
✗ …and carries the case it never had: a RETIRED tier is not an exhausted one …
✗ the tier constant's VALUE is spelled in exactly ONE site … (found: …/contract-review.md:60, …/dispatch-gates.mjs:12142)

4 reds out of 1,422 cases decided — the three rulebook pins plus the landed value-site one
— while both ladder pins stayed green. That asymmetry is the demonstration the card
asks for: move the rule text without the constant and the pins red; the halves are bound,
not merely adjacent.

Acceptance notes

Reported to the PM rather than fixed here — all outside the declared file surface
(scripts/pm/dispatch-gates.mjs · .claude/skills/pm-dispatch/):

  • .claude/skills/checklist-test/SKILL.md:103 states 「floor sonnet · default opus ·
    ceiling fable」 as a live rule, not as history. It is Tier S like this diff, so it
    could have ridden along, but it is out of surface.
  • scripts/pm/check-dispatch-gates.mjs:263 defines part of the mandated surface as "the
    fable-mandatory surface" in a live docblock — stale vocabulary, no behaviour.
  • .claude/skills/pm-dispatch/SKILL.md:534 reads 「额度耗尽豁免仅当契约复审档实测不可用才落默认
    判断档」. With the ceiling now equal to the default, that exemption has no room left to
    exempt; it is in surface but it is a rule about the DISPATCH ladder, not the review fuse
    the card names, so it was left for a ruling rather than reinterpreted here.
  • scripts/pm/check-clause2-carriers.mjs consumes CONTRACT_REVIEW_TIER but is not
    derived by dispatch-gates --commands for a diff that changes it — a watch-hint blind
    spot of exactly the shape that file's own docblocks describe. Run by hand here; green.

Generated by Claude Code

…plit RETIRED from EXHAUSTED

CONTRACT_REVIEW_TIER moves to the model id the harness actually serves, read
from the session's own `external_metadata.last_served_model`. The ladder's
ceiling is no longer a word written beside the constant: it is derived from it
(`tierWordOf` / `TIER_CEILING`), so a retirement ruling moves one line and the
rendering follows.

The downgrade fuse gains the case it never had. A quota exemption covers a tier
that is EXHAUSTED and will come back — wait out of the queue, never downgrade,
never self-review. A tier that is RETIRED never comes back, and the two differ
on who may act: a retirement is a maintainer ruling, never a seat's reading.

Self-tests pin the constant and the rendering together: the ladder's ceiling is
compared against the constant, no retired tier word survives any rendering, the
tier's VALUE is spelled in exactly one place across `scripts/pm/**` and
`.claude/skills/pm-dispatch/**`, and the rulebook's fuse carries both arms.

Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Co-authored-by: Claude <noreply@anthropic.com>
…der line itself

The first cut of the two pins was too wide in both directions. The ceiling pin
searched the whole no-mandate rendering, which also carries the clause-②
suspicion line naming the tier by its id; the retired-word guard searched the
provenance rows too, and those quote maintainer rulings verbatim — a record of
what was ruled AT THE TIME stays true however the tier moves afterwards.

So the ceiling pin reads the ladder LINE, and the guard reads the rule lines
with the `- <path> ⇢ <glob>` rows filtered out, with a non-vacuity case proving
the filtered text still carries rules to search.

Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Co-authored-by: Claude <noreply@anthropic.com>
…the one it cannot see

The single-value-site promise was already pinned further down the battery, and
better: that case derives its scan roots from the mandate globs and FINDS the
definition line instead of remembering it. A second copy of one assertion is a
second thing to keep in step, so it goes.

What stays is the question that pin cannot answer. It searches for the tier's
CURRENT value, so a RETIRED id left behind in the rulebook tree matches nothing
and passes — while reading, to anyone grepping, as a live tier rule. The
surviving case asks the rulebook root whether it spells a model id at all.

Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: adc6aebfe22bd3b6ca6680394079e64bc2f2934f

① Derived judgments

Own worktree at the head sha, exit codes captured before any pipe, nothing adopted from the report.

1 — the constant's new value is the id the harness serves. RIGHT. I read it myself from the primary source — the session harness's own get_session tool, field external_metadata.last_served_model for this session, with session_context.model and configured_model agreeing — and compared it to the export programmatically rather than by eye, by importing the module and evaluating the identity against the string I had read. It is true. ⛔ The id is not written in this record, per the rule the whole card turns on.

2 — the accept set and the public surface the diff moves, item by item. ① The exported CONTRACT_REVIEW_TIER changes VALUE, not shape. ② Three NEW exported symbols appear — tierWordOf, TIER_CEILING, RETIRED_TIER_WORDS — and Clause-②: no survives them: references/contract-review.md:12's mechanical floor (「新导出符号…恒 yes」) is scoped by SKILL.md's 「条款②只指已发布契约面」, and this is an internal tool. Derived, ⛔ not recalled: --tier on the two changed paths returns no path mandate and raises no clause-② suspicion, and check-governed-merges --test puts exactly one path on the register — Tier S, .claude/** ×1 — with no Tier H path. That run warns STALE TREE, so I did not leave it there: origin/main has moved since, and I byte-compared its GOVERNED_SURFACES against the head's — identical, six surfaces, .claude/** Tier S — so the Tier S call holds against the register the landing gate will actually read. Nothing under packages/spec or published skills/ moves. ③ Two rendered strings in tierLines change. ④ One rulebook line changes. ⑤ Ten self-test cases are added.

3 — the ceiling is genuinely derived. RIGHT, and stronger than the report claims. TIER_CEILING = tierWordOf(CONTRACT_REVIEW_TIER) is construction, so inside this file the two halves cannot drift at all; the four new ladder cases are belt-and-braces over that, not the guarantee. The safe-failure choice is sound and the hole it would open is already shut: tierWordOf handing an unparseable id back verbatim is only safe because the companion case forbids the constant from being a bare family word, which is the one input that would make the verbatim path look plausible.

4 — references/contract-review.md:60 paid in place, and ⛔ nothing ruled was lost. RIGHT. The old line carried three clauses and all three survive, compressed: 「豁免对象是派发」 ⇒ 「只及派发」; 「复核 ⛔ 不随派发档位免除」 ⇒ 「⛔ 不及复核」; and 「⛔ 不适用…降档」 follows from the exemption not reaching the review at all. The report's claim that the exhaustion TREATMENT was never on this line checks out against the file, not against the report: 「队列外等档」 is line 53 and 「⛔ 不自审」 is line 26, and the diff is a one-line replacement that touches neither. I ran the ratchet rather than trusting the arithmetic — check-skill-line-ratchet reports the file at 60 lines against ceiling 60, headroom 0, widest table row 0 against pin 0, exit 0. ⛔ No ceiling was raised. Line 60 measures 115 bytes against the 120-byte cap.

5 — the SKILL.md half of the deliverable is correctly empty. .claude/skills/pm-dispatch/SKILL.md:515 already read 「上限 = 契约复审档(CONTRACT_REVIEW_TIER)」, so the ladder there names the constant and follows it. That half was not skipped; it had nothing to do.

6 — the rendered EXITS text and its docblock. RIGHT as text, but see ③(3). No changed sentence is false. An agent reading only the changed lines is told the right thing: a retirement is a maintainer ruling and ⛔ never its own reading.

What I tried that did NOT break it

  • The battery, reproduced rather than accepted. node scripts/pm/dispatch-gates.mjs --self-test at this head: ✓ dispatch-gates self-test: 1893 cases pass., exit 0, zero reds, run to completion in my own worktree.
  • The ablation the report did not run — revert the RULE and nothing else, with no model id appended. Its ablation C confounded two mutations, so it could not tell whether the rulebook pins bind the RULE or only the appended id. I restored line 60 to its exact pre-card text; the file's blob returns to 5c729d04ce, which is the diff's own index base, so the mutation is byte-exact and anchor-checked (1 ⇒ 0). Result, run to completion: ✗ dispatch-gates self-test: 2 of 1893 case(s) failed., exit 1 — exactly 2 reds, both fuse arms — 「…its fuse keeps the quota exemption pointed at DISPATCH」 and 「…carries the case it never had」 — while both ladder pins, the retired-word guard, the no-model-id pin and the constant-name pin all stayed green. Unconfounded, the asymmetry holds and the rulebook half is pinned on its own merits.
  • A second ablation the report did not run — move the constant to a DIFFERENT live family, touch nothing else (anchor 1 ⇒ 0, blob 1c17c94d0d ⇒ f777a6c25e). ✓ dispatch-gates self-test: 1893 cases pass., exit 0 — zero reds, anywhere. That is the design, not a defect — the value is a maintainer ruling and no tree can know what a harness serves — but it is worth stating plainly: the battery binds COUPLING, ⛔ never correctness. Nothing in this repo would have caught a wrong id here. The only thing standing behind the value is the seat's reading and this record's independent re-reading, which is why ①(1) was done from the primary source and not from the report.
  • The claim machinery, read rather than assumed. check-clause2-carriers.mjs is byte-identical to the merge base, and its own battery reproduces at this head — 1115 cases pass, exit 0, zero reds. --template exits 0 and prints Served-tier: with the constant's NAME and ⛔ no model id. Its acceptance code requires the token to equal the NAME exactly — no family, no prefix floor — and MODEL_IDENTIFIER_FORM refuses the NEW value exactly as it refused the old one, with the refusal never echoing the token back. ⇒ ⛔ Nothing in this diff opens a path to claiming a tier that was not served. The tier reading was never this line: fuse line 54 puts it in the transcript harness's per-request stamp, and the diff touches neither line 54 nor line 55 nor --pair.
  • Tree-wide footprint of the new value: exactly two sites, the second a pre-existing self-test fixture outside the pin's two roots, so the one-site pin holds and is not accidentally vacuous.
  • The guard's radius, read in the source. liveRuleText filters lines matching indentation-then-dash, which is exactly the provenance and suspect rows. The three rendering shapes it does cover (no-mandate, mandatory, catalog-barred) are all really exercised, and the suspicion branch really fires — I rendered all four fixtures and checked. Two of the four are textually identical, so the guard covers three distinct shapes, not four.

Where I had to correct the report

The one surviving retired id is left for the right reason, by the wrong argument. The report calls scripts/check-commit-card-trailers.mjs:848 "a rejection vocabulary — the list of ids the gate must refuse … deleting the retired entry would weaken the gate for old history." That mechanism is wrong. Line 848 is a self-test FIXTURE loop; the gate binds a SHAPE at line 320, and that file's own battery carries a case proving "a model word nobody has shipped binds too". Deleting the fixture would cost the gate ⛔ no catching power at all. Leaving it is still right, for the other reason: the comment above it declares the four spellings measured, 「each as the harness wrote it」, and a measured historical fixture is not a live rule. Conclusion right, reasoning wrong — recorded so the next retirement does not inherit the bad argument.

Judging each class of survivor

  • Test-case NAMES and the fableOf helper (three cases, one local) — ⛔ not live rules. They label assertions that compare against CONTRACT_REVIEW_TIER, so the behaviour is tier-agnostic, and a self-test label instructs no agent. Stale vocabulary only; leaving them is right.
  • Verbatim maintainer rulings in the docblocks and in the published-catalog glob's why row, and the incident note — still true as records, and AGENTS.md exempts a verbatim maintainer ruling preserved as a quotation. The why row is the only one that reaches a reader LIVE, since it renders into the claim comment; it is dated, marked verbatim, and sits directly under a MANDATORY line naming the current tier, so it does not mislead. Correctly left.
  • The rejection fixture — above.
  • One class the report files as out-of-surface that nonetheless narrows this PR's headline claim — ③(2).

② Semver level

none. No published package surface moves: the diff is scripts/pm/dispatch-gates.mjs (repo tooling, not a workspace package) and one file in the internal .claude/ agent tree, which is never the published skills/ root. The three new exports are on an internal module; the only value that changes is consumed in-repo. skip-changeset is consistent with that, and the Clause-②: no declaration stands after review rather than merely being accepted.

③ Boundary flags

1 — the circularity, declared. This review was served at opus under the maintainer's 2026-09-21 ruling 「fable 没有了」; this PR is the documentation catching up to that ruling and ⛔ is not its authority. I reviewed the rule that governs my own review and found the circularity sound: the diff removes no part of the machinery by which a review's tier is established, adds no way to claim a tier not served, and leaves Served-tier:, --pair and fuse lines 29/54/55/57 untouched. One honest caveat, since it is a real degradation and not a hypothetical: the constant's old value was a near-unique string, so a transcript or tree grep for it was self-evidencing; the new value is also the word every default-tier seat runs at and already appears at a second, unrelated site in the tree. Proof-by-grep is weaker than it was. It does not make the circularity unsound, because the sanctioned reading is the harness's per-request stamp (line 54) and ⛔ never a text grep — but the next card that touches this fuse should say so, because the cheap reading now looks like it still works.

2 — ⚠️ the headline claim is narrower than it reads, and #19544 is NOT discharged tree-wide. "No live rule still names the retired tier" is true of the retired id and true inside the declared surface. It is ⛔ not true of the retired family word: .claude/skills/checklist-test/SKILL.md:103 still prints the ladder with the retired tier as its ceiling, in a Tier S file, as a live dispatch instruction. The report flags it as class b and out of surface, and that call is defensible — but two things must be recorded. First, the line is self-contradicting rather than simply stale: its operative half sends the reader to that run's --tier output and forbids going from memory, and the parenthetical it then supplies is itself a from-memory transcription that now disagrees with the tool it points at. An agent obeying the sentence gets the right tier; an agent reading the gloss gets a retired one. Second, the new guard's radius will never catch it: RETIRED_TIER_WORDS is asserted only over tierLines renderings, and the no-model-id scan is rooted at .claude/skills/pm-dispatch — and it would not fire anyway, since the line spells a family word and no id. I checked the whole tree for coverage: nothing reads that file's ladder text, only line-count ratchets. ⇒ The follow-up card must widen the retired-word scan across .claude/skills/** prose in the same round as it fixes the text, or the next retirement reproduces this exactly. Until then the lane ships one live retired ceiling.

3 — the ceiling now equals the default, and three rules collapse. Leaving SKILL.md:534 is RIGHT; the PR does ⛔ not ship a rule that can no longer mean anything — but a third site the report did not flag is inside the changed file. Judged one by one: (a) SKILL.md:534 「额度耗尽豁免仅当契约复审档实测不可用才落默认判断档,⛔ 不再往下」 — the permissive half now moves nothing, the prohibition still binds, so it collapses in the SAFE direction, the resulting behaviour is what the fuse wants anyway (no downgrade; contract-review.md:53 supplies 「队列外等档」), and rewriting it would have been choosing a tier policy, which is precisely the seat action the rule this PR lands forbids. Editing it here would have been the fuse's own failure mode. (b) The rendered quota exemption is degenerate for the same reason and equally safe. (c) ⚠️ Not flagged by the report and inside the declared surface: the one-line-class exit's compensating control renders as a skill-face review at the contract-review tier while the exit itself drops execution to the default tier — so the compensation is now at the SAME tier as the work it compensates, and for pure one-liners the floor stays sonnet against a default-tier reviewer where it used to be a strictly higher one. The sentence is not false and the compensation survives as INDEPENDENCE, which line 32 and the Implemented-by: / Reviewed-by: pair still enforce — but its tier half is now empty and nothing in the tree says so. All three are the ruling's arithmetic, ⛔ not defects of this diff. ⇒ For the maintainer, not a seat: is there still a ceiling above the default, and if not, what compensates a one-line-class downgrade?

4 — the second quotation's provenance. The new docblock records the ruling as 「fable 没有了」 followed by 「改成 opus」. The first is marked verbatim on the card body; the second appears only in the same seat's own claim comment on #19544, ⛔ not in any maintainer-authored artifact I could reach, and the card body deliberately marked only the first as verbatim. The substance is not in doubt and no identifier is involved, so no rule is breached — but a governed file now carries a second string in the repo's verbatim-quotation form on single-source provenance. It should be attributed to the claim comment rather than left unlabelled.

5 — the why-row exclusion is a real, bounded hole. A retired word written by hand into a provenance string would render live into a claim comment and ⛔ would not red, because the guard filters exactly those rows. The exclusion is correct — those rows quote maintainer rulings the card forbids rewriting — and the non-vacuity case keeps it from clearing an empty string. Recorded as the attack that came closest without breaking it, and as the true bound on the guard's promise.

6 — the two remaining reported findings, accepted as filed. scripts/pm/check-dispatch-gates.mjs:263 is stale docblock vocabulary with no behaviour; it names the mandated SURFACE, not the tier, and does not mislead about what serves today. The check-clause2-carriers watch-hint blind spot is real and correctly described as the shape that file's own docblocks already name; I re-ran the consumer independently and it is green, so nothing is riding on the derivation gap in this round.

7 — no open questions from the dev to answer. open_questions is empty and the four out_of_scope_findings are each answered above.

Implemented-by: claude/issue-19544-contract-review-tier-off-fable
Reviewed-by: session_01AmH9bKvGoLjiY86Q4Z3og2

VERDICT: PASS


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 21, 2026 10:22
@os-steve
os-steve enabled auto-merge September 21, 2026 10:22
@os-steve
os-steve added this pull request to the merge queue Sep 21, 2026
Merged via the queue into main with commit 77df0f6 Sep 21, 2026
44 checks passed
@os-steve
os-steve deleted the claude/issue-19544-contract-review-tier-off-fable branch September 21, 2026 10:47
This was referenced Sep 21, 2026
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…tant and the retired-word list (objectstack-ai#19684)

Fixes objectstack-ai#19680

Clause-②: no

The contract-review tier is fable. PR objectstack-ai#19573 moved
`CONTRACT_REVIEW_TIER` to the opus id and filled `RETIRED_TIER_WORDS`
because one agent's first request at the tier came back HTTP 429 on
2026-09-21. The maintainer's ruling of 2026-09-22 (issue comment
5771798588) is that this was a temporary lack of authorization on one
session, not a retirement, and that it must not have changed the skills
— verbatim and untranslated, in order:

> 「复核档应该就是 fable 啊」 · 「某个agent临时没有fable给的特殊授权,不应该改变skills」 · 「fable 撤回卡
你来创建」

## What lands — `scripts/pm/dispatch-gates.mjs` only

1. **The constant.** `CONTRACT_REVIEW_TIER` reads the fable id again, so
`TIER_CEILING` derives back to the fable word and the ladder reads
`floor sonnet · default opus · ceiling fable`.
2. **The retired list is empty.** `RETIRED_TIER_WORDS =
Object.freeze([])` — no tier word is retired. The guard itself stays.
3. **The docblock beside the constants carries the principle**, as one
rule with the ruling quoted by date and comment id: a tier word is
RETIRED only by the maintainer's explicit ruling that NAMES a
retirement; a 429, an exhausted quota or a missing authorization on one
session is never a retirement; and a seat this tier is not served to
renders the review through an isolated at-tier subagent or waits outside
the queue — never by editing the constant. What one session is
authorized for is a property of that session; the constant is a property
of the lane's governance. `SKILL.md:516` points every tier value at this
file, so this docblock is that principle's home.
4. **The non-vacuity pin moved off the live list onto a MUTATED copy**
(see below).

**Kept, deliberately — these were never the defect.** `tierWordOf()`,
the derived `TIER_CEILING`, the retired-spelling guard, and every
rendering that reads the constant (the ladder, the exits, the clause-②
note, the suspicion line) are PR objectstack-ai#19573's structurally sound half:
deriving the ceiling from the constant is exactly what stops the pair
drifting, in either direction. The exits text is tier-agnostic and reads
`TIER_DEFAULT`, so it follows the constant with no edit.

**Not touched:** `.claude/**` (step ② of objectstack-ai#19061 owns the reference
text), `scripts/check-commit-card-trailers.mjs:848` (a rejection
vocabulary that must keep listing the id), any workflow.

## The pin that could not survive the revert, and what replaced it

`the retired-spelling guard is not vacuous — it names at least one word`
cannot hold on an empty list, and an empty list clears every rendering
for free. Counting entries on the LIVE list is therefore not available
as a control any more. The guard now runs through one function that both
legs call, and the non-vacuity case is proved on a **mutated copy**:
feed the guard a word the ladder demonstrably prints and it must red.
Three cases now stand where one stood:

```
✓ ⛔ no RETIRED tier word survives in any live RULE — … (dirty: none)
✓ no tier word is RETIRED today — the list is empty and frozen, which is what
  "a temporary lack of authorization is not a retirement" looks like in data (holds: none)
✓ …and the guard is NOT vacuous, proved on a MUTATED copy: a list naming the ladder's own
  ceiling word (fable) REDS, so the green above is the empty list and not a broken search
  (mutated hits: 0/fable 1/fable 2/fable 3/fable)
✓ …and the invariant that copy stands for: no tier word still in the ladder may ever enter the live list
```

The mutated-copy control fires on all four renderings, printed on every
run — so the guard's green is measured on every run, not only in the
one-off ablation below.

## Evidence

**The two `--tier` renders, verbatim, at `c39a60e`** (`node
scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier
PATH`, exit 0 both):

```
Model tier — MANDATORY: claude-fable-5-1 (derived from the file surface, not recalled).
```

```
Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s), derived here, not recalled.
  The tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling fable).
```

**`pnpm check:pm-dispatch-gates`** — `EXIT=0`, `✓ dispatch-gates
self-test: 1906 cases pass.`, zero reds, run to completion on the final
commit `c39a60e`. Run detached per this file's own header (the battery
re-spawns the tool's CLI and took 675.0s on this box, over the
container's foreground cap) with the exit code captured to a file, never
through a pipe.

**Derived gate families** — `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at the FINAL head derived 29
commands (byte-identical to the derivation taken at the first commit).
All 29 were run, each exit code captured before any pipe, and handed
back through `--ran`:

```
✓ dispatch-gates --ran: 29 derived famil(ies) accounted for — 29 run,
  0 NOT-MEASURED (a DERIVED zero — all 29 recorded an exit code and none of them is 3).
```

Four of them (`check-ci-filter-parity`, `check-closing-keyword-parity`
and its self-test, `check-comment-mask-corpus`) first exited **3
`PREREQUISITE NOT MET`** in a worktree with no `node_modules`. That is
NOT MEASURED, never a pass and never a finding — they were re-run green
after `pnpm install`, and only the second reading is recorded above.

**Run by hand, outside the derivation** — `node
scripts/pm/check-clause2-carriers.mjs --self-test` → exit 0, `1140 cases
pass`. It imports `CONTRACT_REVIEW_TIER`, so this change moves its
verdict, and the path derivation does not name it (the watch-hint blind
spot PR objectstack-ai#19573 reported).

**Governed-surface predicate** — `node
scripts/pm/check-governed-merges.mjs --test
scripts/pm/dispatch-gates.mjs` → exit 0, `governed-surface predicate: 0
of 1 path(s) hit the register (6 surfaces, repo-agnostic)`, `NOT
governed — ordinary queue landing applies to a PR with exactly this file
list`. Head repo and size are NOT MEASURED by `--test`; re-read on the
PR's own number before landing.

**Changed lines** — 62 (+50 / −12, 1 file), under the 5000 human-merge
threshold.

**Lint, narrowed and declared.** `eslint scripts/pm/dispatch-gates.mjs
--no-inline-config` → 0 errors, 0 warnings. The narrowing is measured,
not assumed: ① eslint's own population is
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` (`eslint.config.mjs:971`), so the
population is the JS/TS tree; ② the file count is read from `--format
json` (1); ③ `eslint.config.mjs:327` states the config enables no
type-aware linting for ANY file (no `parserOptions.project`, no typed
rules), so this diff cannot move the verdict on a file it does not
touch. The repo-wide `pnpm lint` sweep is CI's run.

**Control bytes.** `grep -naP` for the control class over the changed
file prints nothing (exit 1); the control — the same `-P` engine asked
for printable ASCII on the same file — fires on 28,427 lines. `pnpm
check:nul-bytes` exit 0.

**Changeset — `skip-changeset`, measured.** Every one of the 70
published packages in this workspace declares an explicit `files[]`, and
the distinct entries across all of them are `CHANGELOG.md`, `README.md`,
`api-surface`, `dist`, `json-schema`, `liveness`, `llms.txt`, `prompts`,
`spec-changes.json`, `src/**/*.zod.ts`. Not one reaches `scripts/`, so
`scripts/pm/dispatch-gates.mjs` ships in no tarball and this diff
publishes nothing.

### Reverse verification — the live guard can still fail

Landed through `scripts/ablation-replace.mjs` (anchor count checked,
blob hash recorded before and after), the direction declared BEFORE the
run: put the fable word back into the retired list, expect exactly three
reds in the retired-guard block and no others.

```
ablation-replace: ok mutation landed: anchor 1 -> 0, blob f04cb35 -> 2d7ada7ab81a
  ✗ ⛔ no RETIRED tier word survives in any live RULE — … (dirty: 0/fable 1/fable 2/fable 3/fable)
  ✗ no tier word is RETIRED today — the list is empty and frozen … (holds: fable)
  ✗ …and the invariant that copy stands for: no tier word still in the ladder may ever enter the live list
✗ dispatch-gates self-test: 3 of 1906 case(s) failed.
ablation-replace: ok restored: blob == HEAD (f04cb35) and `git diff HEAD` is empty
```

Three reds out of 1906, exactly the three predicted and no others — the
guard, the emptiness reading and the invariant are three different
halves of one promise, and each one binds. The restore is proved by blob
equality against HEAD and an empty `git diff HEAD`, not by an exit code.

## 维护者速读(草稿)

**改了什么** — 复核档常量改回 fable;退役词表清空;常量旁的注释块写下这条原则的正文(含 2026-09-22
三句裁决原话与评论号);那条「表里至少有一个词」的自检钉不可能在空表上成立,改成在变异副本上证明守卫仍会红。只动
`scripts/pm/dispatch-gates.mjs` 一个文件。

**为什么改** — 2026-09-21 把常量从 fable 改成 opus 的依据,是某个 session 的 HTTP
429。那是「这一个 agent 此刻没拿到授权」,不是「这个档位退役了」。把前者写进常量,等于让一次临时授权缺失改写整条车道的治理值。维护者
2026-09-22 明确撤回。

**风险与代价(含回滚)** — 风险低:改动只在一个未受管文件里,29 个派生门禁族全绿、自检 1906
例全过,并做了消融证明守卫仍会红。代价是复核档重新高于默认档,席位若当时拿不到 fable,须走隔离子 agent 或在队列外等档,⛔
不得自行降档。回滚就是把这两行改回去 —— 但那正是本卡禁止的动作,除非维护者另行裁决退役。

**席位意见** — (留空,待席位定稿)

**你要做的** — 确认这条原则的措辞就是你的意思:「档位退役只认你点名退役的裁决;429/额度/单 session
无授权都不算退役」。确认后本 PR 可照常走队列落地。

## Acceptance notes

Reported, not fixed here — all outside this card's declared file surface
(`scripts/pm/dispatch-gates.mjs`):

- **Three of PR objectstack-ai#19573's own acceptance notes are cleared by this revert
without an edit**, and are recorded here so nobody files them again:
`.claude/skills/checklist-test/SKILL.md:103` states 「floor sonnet ·
default opus · ceiling fable」 as a live rule — the ladder prints exactly
that again; `scripts/pm/check-dispatch-gates.mjs:263` calls the mandated
surface "the fable-mandatory surface" — accurate again;
`.claude/skills/pm-dispatch/SKILL.md:538` 「额度耗尽豁免仅当契约复审档实测不可用才落默认判断档」
had no room left to exempt while the ceiling equalled the default, and
has room again now.
- **A forward interaction worth naming before objectstack-ai#19061 step ② lands.** Two
self-test cases in this file read
`.claude/skills/pm-dispatch/references/contract-review.md` and assert
its 降档保险丝 lines (「额度耗尽豁免只及派发 ⛔ 不及复核」 and 「档位退役 ≠ 耗尽,恒维护者裁决 ⛔ 非席位读数」).
Deleting that section per the maintainer's 「降档保险丝 不留」 turns both red.
That is objectstack-ai#19061's to carry — its diff must retire those two pins in the
same PR. Content-wise the deleted line stays true of this change; it is
only the pins that need moving.
- `scripts/pm/check-clause2-carriers.mjs` consumes
`CONTRACT_REVIEW_TIER` but is still not derived by `dispatch-gates
--commands` for a diff that changes it — the same watch-hint blind spot
PR objectstack-ai#19573 reported, unchanged. Run by hand here; green.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Wnstp2kTth7sGXfr8fXypc)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants