Repository navigation
Commit 372931e
pm(tooling): the contract-review tier is fable — revert the opus constant and the retired-word list (#19684)
Fixes #19680
Clause-②: no
The contract-review tier is fable. PR #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 #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 #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 #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 #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 #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 #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 #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>1 parent eea7ccc commit 372931e
1 file changed
Lines changed: 50 additions & 12 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12243 | 12243 | | |
12244 | 12244 | | |
12245 | 12245 | | |
12246 | | - | |
| 12246 | + | |
| 12247 | + | |
12247 | 12248 | | |
12248 | 12249 | | |
12249 | 12250 | | |
| |||
12254 | 12255 | | |
12255 | 12256 | | |
12256 | 12257 | | |
12257 | | - | |
12258 | | - | |
12259 | | - | |
12260 | | - | |
12261 | | - | |
| 12258 | + | |
| 12259 | + | |
| 12260 | + | |
| 12261 | + | |
| 12262 | + | |
| 12263 | + | |
| 12264 | + | |
| 12265 | + | |
| 12266 | + | |
| 12267 | + | |
| 12268 | + | |
| 12269 | + | |
| 12270 | + | |
| 12271 | + | |
| 12272 | + | |
| 12273 | + | |
| 12274 | + | |
| 12275 | + | |
| 12276 | + | |
| 12277 | + | |
| 12278 | + | |
12262 | 12279 | | |
12263 | 12280 | | |
12264 | 12281 | | |
12265 | | - | |
| 12282 | + | |
12266 | 12283 | | |
12267 | 12284 | | |
12268 | 12285 | | |
| |||
12506 | 12523 | | |
12507 | 12524 | | |
12508 | 12525 | | |
12509 | | - | |
| 12526 | + | |
12510 | 12527 | | |
12511 | 12528 | | |
12512 | 12529 | | |
12513 | 12530 | | |
12514 | 12531 | | |
12515 | | - | |
| 12532 | + | |
| 12533 | + | |
| 12534 | + | |
| 12535 | + | |
| 12536 | + | |
| 12537 | + | |
| 12538 | + | |
| 12539 | + | |
| 12540 | + | |
12516 | 12541 | | |
12517 | | - | |
| 12542 | + | |
12518 | 12543 | | |
12519 | 12544 | | |
12520 | 12545 | | |
| |||
25476 | 25501 | | |
25477 | 25502 | | |
25478 | 25503 | | |
25479 | | - | |
| 25504 | + | |
| 25505 | + | |
| 25506 | + | |
| 25507 | + | |
| 25508 | + | |
| 25509 | + | |
| 25510 | + | |
| 25511 | + | |
| 25512 | + | |
| 25513 | + | |
| 25514 | + | |
| 25515 | + | |
25480 | 25516 | | |
25481 | | - | |
| 25517 | + | |
| 25518 | + | |
| 25519 | + | |
25482 | 25520 | | |
25483 | 25521 | | |
25484 | 25522 | | |
| |||
0 commit comments