Repository navigation
pm(tooling): move the contract-review tier off the retired ceiling, and give the fuse the case it never had - #19573
Conversation
…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>
Contract reviewServed-tier: ① Derived judgmentsOwn 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 2 — the accept set and the public surface the diff moves, item by item. ① The exported 3 — the ceiling is genuinely derived. RIGHT, and stronger than the report claims. 4 — 5 — the 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
Where I had to correct the reportThe one surviving retired id is left for the right reason, by the wrong argument. The report calls Judging each class of survivor
② Semver levelnone. No published package surface moves: the diff is ③ Boundary flags1 — 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 2 — 3 — the ceiling now equals the default, and three rules collapse. Leaving 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 6 — the two remaining reported findings, accepted as filed. 7 — no open questions from the dev to answer. Implemented-by: VERDICT: PASS Generated by Claude Code |
…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>
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_TIERinscripts/pm/dispatch-gates.mjsnow holdsthe opus model id. ⛔ Not recalled and ⛔ not invented: read off the harness itself — the
claude-code-remoteget_sessiontool reports it for this session asexternal_metadata.last_served_model, andsession_context.modelandconfigured_modelagree 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_CEILINGis thatderivation — so
--tierrendersand 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".
tierWordOfhands an id it cannot read back VERBATIM rather thanthrowing:
--tierruns on every dispatch, and a ladder printing the whole id is louderthan one printing a family word nobody ruled.
3 — the case the rule never had.
.claude/skills/pm-dispatch/references/contract-review.mdline 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, therulebook 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'swhyrow,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 aretier-agnostic in behaviour). The retired-word guard therefore covers the RULE lines and
excludes the
- path ⇢ glob — whyprovenance rows, with a non-vacuity case proving thefiltered text still carries rules to search.
check-clause2-carriers --template'sServed-tier:contract is unchanged: the keystill 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
No Tier H path is touched:
AGENTS.md,CLAUDE.md,docs/adr/**,docs/NORTH-STAR.mdand the published
skills/root are all outside this diff. This PR lands on the owningseat'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 thefinal 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 --commandsderived 37commands off the merge base; all were run and all are green, with one NOT MEASURED:
check:doc-formula-expressionsexits 3PREREQUISITE NOT MET(two workspace packages areunbuilt in this worktree) — ⛔ read as not measured, never as a pass.
check:pm-skill-ratchetand its self-test are green (157 cases), as are
check:pm-governed-merges,check:skill-frame-sync,check:watch-hint-literal,check:nul-bytesandcheck: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.mdfile is outside itentirely; ② the file count is read from
--format json(1); ③eslint.config.mjsenablesno type-aware linting anywhere (no
parserOptions.project, noprojectService), so thisdiff cannot move the verdict on any file it does not touch. The repo-wide
pnpm lintsweepis CI's run.
Control bytes.
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over both changed filesprints nothing (exit 1); the control — the same
-Pclass engine asked for printable ASCIIon 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:
The control proves the grep fires: on
origin/mainit finds the constant's olddeclaration; on this branch that line is gone. The one survivor is a rejection
vocabulary — the list of ids
check-commit-card-trailersmust 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, blobhash before/after recorded) and restored with
git checkout HEAD --, proven byblob == HEAD bloband an emptygit diff HEAD.A — move the code side without the rule. The ladder's
ceiling ${TIER_CEILING})wasreplaced by the retired family word (anchor 1 → 0, blob
e3a804cfc19f→d19c861ac7ee):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):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):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:103states 「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:263defines part of the mandated surface as "thefable-mandatory surface" in a live docblock — stale vocabulary, no behaviour.
.claude/skills/pm-dispatch/SKILL.md:534reads 「额度耗尽豁免仅当契约复审档实测不可用才落默认判断档」. 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.mjsconsumesCONTRACT_REVIEW_TIERbut is notderived by
dispatch-gates --commandsfor a diff that changes it — a watch-hint blindspot of exactly the shape that file's own docblocks describe. Run by hand here; green.
Generated by Claude Code