Skip to content

pm(tooling): the contract-review tier moves off the retired Fable model to opus, and the rule gains the case it never had — a tier that is GONE is not a tier that is exhausted #19544

Description

@os-steve

Unblocks: #19542 (open p1, its PR #19517 cannot be reviewed), and every future domain:spec and domain:skills delivery — the lane owes an at-tier contract review on EVERY round it delivers.

Filed by the domain:spec seat 4 (session_01AmH9bKvGoLjiY86Q4Z3og2, seat post #18917) on 2026-09-21, on the maintainer's ruling below. ⛔ Not a claim.

The ruling

Asked which tier serves the contract review now that Fable is gone, the maintainer chose opus, on this reading of the skill's own ladder: 「floor sonnet · default opus · ceiling fable」 — with fable gone, the ceiling falls to opus.

Their words on the situation, verbatim: 「fable 没有了」.

Why this is not something a seat could decide

reading value
scripts/pm/dispatch-gates.mjs:12111 export const CONTRACT_REVIEW_TIER = 'claude-fable-5-1'
references/contract-review.md:26 按车道:spec 与 skills 席审契约增量;达档席内审,未达档 ⛔ 不自审,起达档子代理
references/contract-review.md:60 契约复核 ⛔ 不适用额度耗尽豁免降档:豁免对象是派发,复核 ⛔ 不随派发档位免除
references/contract-review.md:53 起不来即无复核,标签原样、队列外等档

⭐ The written rule covers 额度耗尽 — a tier that is exhausted and will come back. It does ⛔ not cover a tier that no longer exists. Two dispatches of one review died on their first request (HTTP 429, 「You've reached your Fable limit」, model claude-fable-5-1) before the maintainer confirmed the model is gone. A seat reading 「不许降档」 onto a vanished tier would hold the whole lane forever; a seat picking a replacement on its own would be exactly the silent downgrade that fuse exists to stop. ⇒ it went to the maintainer, and this card carries their answer.

The deliverable

  1. scripts/pm/dispatch-gates.mjs:12111 — CONTRACT_REVIEW_TIER moves to the opus model id. ⛔ Do not invent the string: read the id the harness actually serves and pin it, and ⛔ do not leave a second spelling of the old id anywhere the checkers read.
  2. The skill text that names the ladder — .claude/skills/pm-dispatch/SKILL.md and references/contract-review.md, wherever 「ceiling fable」 or the fable id appears as a live rule rather than as history. ⚠️ ⛔ Do NOT rewrite records of past rulings that legitimately name fable: a record of what was served then stays true.
  3. A line for the case the rule did not have: a tier that is gone is not a tier that is exhausted, and the difference is who may act. ⛔ 额度耗尽 keeps its existing treatment (wait, ⛔ never downgrade); a retired tier is a maintainer ruling, ⛔ never a seat's reading.
  4. Self-tests must pin the new constant and the ladder text together, so the two cannot drift apart again.

⚠️ .claude/** is Tier S: this PR lands on the owning seat's at-tier review of record, ⛔ no approval is owed and ⛔ no maintainer click is waited for. ALL-not-ANY — one Tier H path would make the whole PR Tier H, so ⛔ do not touch AGENTS.md, CLAUDE.md, docs/adr/**, docs/NORTH-STAR.md or published skills/** in this diff.

Acceptance

node scripts/pm/dispatch-gates.mjs --self-test green with a case that fails if the constant and the skill's ladder text disagree; check-clause2-carriers --template prints the unchanged Served-tier: contract (the key admits the constant name, ⛔ never a model id, and that does not change); and a grep showing no live rule still names the retired id, with a control proving the grep fires.

查重词

CONTRACT_REVIEW_TIER fable retired · contract review tier opus · ceiling fable ladder · tier gone versus tier exhausted · 降档保险丝 档位消失


Generated by Claude Code

Activity

  1. self-assigned this
    on Sep 21, 2026
  2. os-steve commented on Sep 21, 2026

    @os-steve
    CollaboratorAuthor

    Claim: PM loop round 15
    Session: session_01AmH9bKvGoLjiY86Q4Z3og2
    Branch: claude/issue-19544-contract-review-tier-off-fable
    Worktree: objectstack-issue-19544
    Domain: domain:spec
    Seat: domain:spec#4
    File surface: scripts/pm/dispatch-gates.mjs · .claude/skills/pm-dispatch/ (stop on breach; explain in the report)
    Container & model: S 级机械卡, mode:subagent, model: opus
    Clause-②: no
    Thread-read: none
    Serial constraints cleared: none — no open PR touches scripts/pm/dispatch-gates.mjs or the two skill files, read off the open-PR file lists at this claim.


    Graded p1 by this seat and ⛔ not by triage, stated so it can be revised: this card carries an Unblocks: line naming an open p1 whose PR cannot be reviewed, and under the landed charter a tooling card with an Unblocks: 「继承产品级不计数」 — so it neither waits behind the fleet's one-tooling-card cap nor spends it. ⚠️ If triage disagrees with the grade, the grade is theirs.

    ⭐ The review this card unblocks is already running at the new tier, at 2026-09-21T08:18Z. The maintainer's ruling is the authority for that, ⛔ not this card landing first: 「改成 opus」 redefines what 「at tier」 means from the moment it was given, and this PR is the documentation catching up to it. ⛔ A seat may not reverse that order on its own — the fuse exists to stop a seat downgrading silently, and a maintainer ruling is the one thing it defers to.

    ⚠️ .claude/** puts this at Tier S: it lands on this seat's at-tier review of record, ⛔ no approval is owed and ⛔ no maintainer click is waited for.


    Generated by Claude Code

  3. os-steve commented on Sep 21, 2026

    @os-steve
    CollaboratorAuthor

    os-dev-report

    {
    "issue": 19544,
    "status": "done",
    "branch": "claude/issue-19544-contract-review-tier-off-fable",
    "pr": "#19573",
    "session": "session_01AmH9bKvGoLjiY86Q4Z3og2",
    "premise_still_valid": true,
    "summary": "CONTRACT_REVIEW_TIER moves to the opus tier the harness serves, the ladder ceiling is now DERIVED from that constant instead of written beside it, and the downgrade fuse gains the RETIRED-vs-EXHAUSTED distinction in place. WHERE THE ID WAS READ: the claude-code-remote get_session tool, field external_metadata.last_served_model for this session, with session_context.model and configured_model agreeing and the runtime statement of the model it runs on matching; nothing was recalled or invented. The value itself is not spelled in this comment, per the AGENTS.md rule that no model identifier lands in a comment. RETIRED ID: CHANGED at exactly one site, scripts/pm/dispatch-gates.mjs:12124, the constant value. LEFT at scripts/check-commit-card-trailers.mjs:848, which is the vocabulary that gate REFUSES in a commit trailer, not a tier rule; deleting the entry would stop it catching the id in old history, and it is outside the declared file surface anyway. FAMILY WORD AS A LIVE RULE, CHANGED: the ladder line, and the mandatory EXITS rendering plus its docblock, which read 'fable unavailable => opus, never lower' and now read 'the mandated tier EXHAUSTED => opus, never lower - a tier that is RETIRED is a maintainer ruling instead, never a seat reading'. FAMILY WORD LEFT AS HISTORY, judged one by one, all inside dispatch-gates.mjs: the verbatim maintainer rulings of 2026-08-16, 2026-09-10 twice (one of them the skills/** glob rendered why row), 2026-09-17 and the accept-your-suggestion note; the incident note in the battery; the quoted incident claim about the fable-mandatory roots; and the new docblock own verbatim 2026-09-21 quote. A record of what was ruled AT THE TIME stays true, which the card states explicitly. FAMILY WORD LEFT AS TEST LABELS, in surface: the fableOf helper and three case names calling the mandate fable-mandatory. They already assert against CONTRACT_REVIEW_TIER, so they are tier-agnostic in behaviour; a case NAME is not a live rule, and renaming them would touch lines the card does not need. The retired-word guard therefore covers the RULE lines of every rendering and excludes the provenance rows, with a non-vacuity case proving the filtered text still carries rules to search. PAID IN PLACE: contract-review.md is still 60 lines against its ceiling of 60 at headroom 0, and the new line is 115 bytes against the 120-byte cap. No ceiling was raised and no ruled clause deleted; the old line two clauses survive compressed, and the exhaustion treatment was never on that line at all - waiting out of the queue is line 53 and never-self-review is line 26, and neither moved. check-clause2-carriers --template still prints Served-tier: CONTRACT_REVIEW_TIER, the constant NAME and never a model id. It was already pinned, so nothing was added there, only re-run.",
    "tests": "node scripts/pm/dispatch-gates.mjs --self-test => 1893 cases pass, exit 0, zero reds, run to completion on the final commit (detached per the file own header; 855.1s). | pnpm check:pm-dispatch-gates, the wrapper CI runs => 'the exit contract holds in all three directions' plus 1893 cases pass, zero reds. | node scripts/pm/check-clause2-carriers.mjs --self-test => 1115 cases pass, exit 0. It imports the constant, so this change moves its verdict. | node scripts/pm/check-clause2-carriers.mjs --template => exit 0, Served-tier: CONTRACT_REVIEW_TIER unchanged. | node scripts/pm/check-governed-merges.mjs --test with the two changed paths => Tier S only (.claude/** x1, contract-review.md); dispatch-gates.mjs is not on the register; no Tier H path. main has since moved that script but NOT its GOVERNED_SURFACES register, checked by diff. | Derived families: --commands derived 36 and --ran accounted for all 36 at exit 0: 35 run green, 1 NOT MEASURED (check:doc-formula-expressions exits 3 PREREQUISITE NOT MET because two workspace packages are unbuilt in this worktree - read as not measured, never as a pass), 0 UNRUN. Re-derived after git fetch origin main: identical family list. | check:pm-skill-ratchet green (157 self-test cases plus the run): contract-review.md 60 lines / ceiling 60 / headroom 0, widest table row 0 against pin 0. | eslint scripts/pm/dispatch-gates.mjs --no-inline-config => 1 file, 0 errors, 0 warnings. Narrowing declared: (1) the eslint population is its own config glob for ts/tsx/mts/cts/js/jsx/mjs/cjs, so the .md file is outside it entirely; (2) the file count is read from --format json; (3) no type-aware linting is configured anywhere (no parserOptions.project, no projectService), so this diff cannot move the verdict on any file it does not touch. The repo-wide sweep is CI run. | Control bytes: grep -naP 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 28205 and 53 lines. | GREP PLUS CONTROL: git grep -nE for the retired id shape on HEAD finds only scripts/check-commit-card-trailers.mjs:848; the identical pattern on origin/main also finds scripts/pm/dispatch-gates.mjs:12124, which proves the grep fires and that the old declaration is exactly what went away. | ABLATION A, code side moves and the rule does not: the ladder derived ceiling replaced by the retired family word via scripts/ablation-replace.mjs, anchor 1 -> 0, blob e3a804cfc19f -> d19c861ac7ee. 2 reds of 1506 cases decided - 'the ladder prints a ceiling DERIVED from the contract-review constant' and 'no RETIRED tier word survives in any live RULE (dirty: 0/fable 3/fable)'. Every other new pin stayed green. | ABLATION B, a second spelling of the tier VALUE under scripts/pm, anchor 1 -> 0, blob e3a804cfc19f -> 517886d4a0f9: red on the LANDED value-site case. That is why the two cases first written here for the same promise were deleted before review - one assertion, one owner. | ABLATION C, rule side moves and the code does not: contract-review.md line 60 reverted to its pre-card wording with a model id appended, anchor 1 -> 0, blob aebe3ca8b11d -> 906f7ea1e9d5. 4 reds of 1422 cases decided - the rulebook-spells-no-model-id pin, both fuse-arm pins and the landed value-site pin - while BOTH ladder pins stayed green. That asymmetry is the demonstration the card asks for: the halves are bound, not merely adjacent. | Every ablation was restored with git checkout HEAD --, each proven by blob == HEAD blob and an empty git diff HEAD; the final tree is clean at the pushed head. | NOT MEASURED: CI on the PR (the report is delivered when local verification is done, per the dispatch contract) and check:doc-formula-expressions as above.",
    "mcp_calls": "1 - mcp__Claude_Code_Remote__get_session, read-only, the harness served-model reading. 0 GitHub MCP calls, and no MCP write tool was used.",
    "api_writes": "3 REST proxy writes: POST /repos/objectstack-ai/objectstack/pulls (draft PR 19573); POST /repos//issues/19573/labels (skip-changeset, via scripts/pm/label-write.mjs, read back and matching); POST /repos//issues/19544/comments (this report). Plus 4 git pushes to the feature branch. No PATCH of the PR body: it was written once at creation and read back byte-identical except the trailing newline the platform strips, carrying exactly one attribution footer.",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: b - .claude/skills/checklist-test/SKILL.md:103 states the ladder as a LIVE rule, with the retired tier as the ceiling, for a tier that no longer exists. Contract text: the same ladder in dispatch-gates.mjs, whose ceiling now derives from CONTRACT_REVIEW_TIER. Seam: spec:CONTRACT_REVIEW_TIER -> runtime:tierLines ladder rendering | consumer: .claude/skills/checklist-test/SKILL.md:103. It is Tier S like this diff so it could have ridden along, but it is outside the declared file surface (scripts/pm/dispatch-gates.mjs and .claude/skills/pm-dispatch/). carrier: none, needs its own card. dedupe words: checklist-test ceiling fable | ladder retired tier skill text | contract review tier opus | floor sonnet default opus",
    "class: a - scripts/pm/check-clause2-carriers.mjs imports CONTRACT_REVIEW_TIER from dispatch-gates.mjs, so a card changing the constant moves that gate verdict, yet dispatch-gates --commands on this very diff does not derive check:pm-clause2-carriers. Repro: node scripts/pm/dispatch-gates.mjs --commands on this branch, and the family is absent. This is the blind-spot shape check-dispatch-gates.mjs already fixed twice for itself by declaring the module as a constant (SURFACE_MODULE and FRAME_MODULE) - an import specifier is not a discoverable watch hint. Run by hand here and green. carrier: none, needs its own card. dedupe words: clause2-carriers watch hint not derived | import specifier watch hint blind spot | CONTRACT_REVIEW_TIER consumer derives nothing | declared watch hint constant",
    "carrier: the next pm-dispatch rules round - .claude/skills/pm-dispatch/SKILL.md:534 says the quota exemption falls to the default tier only when the contract-review tier is measured unavailable. With the ceiling now equal to the default, that exemption has no room left to exempt. It IS inside the declared file surface, but it is a rule about the DISPATCH ladder rather than the review fuse this card names, and rewriting it would be choosing a tier policy rather than implementing the ruling. Noted, not fixed.",
    "carrier: whoever next touches scripts/pm/check-dispatch-gates.mjs - its line 263 docblock defines part of the mandated surface as the fable-mandatory surface. Stale vocabulary only, no behaviour; naming, so noted in the PR acceptance notes rather than filed."
    ]
    }


    Generated by Claude Code

  4. added a commit that references this issue on Sep 28, 2026
    77df0f6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions