feat(pm): gate the clause-② carrier strip on the served tier the verdict declares - #17990
Conversation
`CONTRACT_REVIEW_TIER`'s own docblock declares the comparison against the SERVED tier EXACT, and nothing performed it: the dispatching seat passes a model as a parameter, a parameter is configuration rather than a reading, and the reviewer's "opening self-check" was prose a round could skip while producing a verdict indistinguishable from one that did not. A contract-review verdict now carries a `Served-tier:` line whose value is the harness-stamped served-model field of the reviewer's own transcript, and `check-clause2-carriers.mjs --pair` refuses to treat a `needs:contract-review` pair as cleared unless the newest verdict on the current head declares exactly the constant — naming the PR, the verdict comment and the served value, at exit 4. The row (C7) rides C6's population and C6's chosen comment, so a clearance judgment of a hung carrier is the whole of its scope: a `Clause-②: no` pair that never carried the label is never refused for lacking the line, and an absent or unsigned record stays C6's row alone. The constant is imported from `dispatch-gates.mjs`, which keeps its single value site. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
C7 compares against `CONTRACT_REVIEW_TIER`, which is declared in `dispatch-gates.mjs` and imported here, so a card editing that constant now predicts this gate — and the marker claiming no card's file surface can predict it stopped being true the moment the import landed. A false declaration is the shape the new row itself exists against, so it is retired rather than kept. The note left in its place states the input half that is still true, the half that is not, and the imprecision the import channel adds on the way: a followed module contributes its own literals, so this family inherits a `.github/workflows` lead it never opens, and the designed narrowing is per-module and cannot express one caller's case while the sibling really does read those globs. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
…rved-tier-gates-the-strip
Measured against the live corpus rather than the fixture: every record the remediation rounds post spells the value as the stamp control first, then the tier — `Served-tier: 75/75 \`<tier>\`` — and the ruling's own specimen is written the same way. A reader that demanded the tier token immediately after the colon would have refused every verdict produced under the rule it enforces, on its first day. So the grammar is `[N/M ]<tier>`, and the count is judged rather than skipped: it is the zero-hit control the discipline already requires, and a count that is not total is the fallback evidence whose own rule text voids the verdict entire. Absent, it is vacuous — the tier alone decides, which is the ruling's minimum, so nothing the ruling permits is refused. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewHead: Files: ① derived judgments — four changes to what the gate accepts or reports, each read against ruling 5651924370 items 1–3:
Seat measurements on the head tree ( ② semver: no package touched; ③ boundary flags: Checks on Implemented-by: Verdict: PASS — items 1–3 of ruling 5651924370 implemented as ruled; nothing widens. Governed ( Generated by Claude Code |
维护者速读(终稿)改了什么 —— 一行规则、一道门。规则:契约复审裁决首行必带 为什么 —— 裁决 5651924370 第 1–3 项:常量 docblock 写着「与服役档精确比较」,树上没人在比;卡上实测多轮在档下清了标。 风险 —— 收紧,不放宽。现网今天已有两条裁决没带这行(#17983、#17979 的记录),各补发一条记录即可,落地前不会被卡。一处顺手:该文件的 一红 —— 批准即由本席按裁决 C 落地;顺手点一次 re-run 即可,或等下一次 push。要不要批? Generated by Claude Code |
|
The red, once more and with a card — skills seat, 2026-09-13T11:29Z. Generated by Claude Code |
|
Green — skills seat (session Generated by Claude Code |
|
Ruling C landing step — skills seat (session Generated by Claude Code |
…ce tiering, ui/triage lane lines (objectstack-ai#17942 chain) (objectstack-ai#18018) Fixes objectstack-ai#17971 Fixes objectstack-ai#17742 Fixes objectstack-ai#17624 Part of objectstack-ai#17942 Part of objectstack-ai#17950 Part of objectstack-ai#17933 Part of objectstack-ai#17934 Part of objectstack-ai#17951 Dev session `session_01DAcomhvR9kKizeYgg89Vo8` on branch `claude/issue-17942-charter-revision` (worktree `objectstack-issue-17942`). Rules-layer charter revision for the eight-card family whose chain head is objectstack-ai#17942. Deferred to the follow-up references PR (files occupied by PR objectstack-ai#17990 / objectstack-ai#17992, seat-landable once this PR lands the tiering): `contract-review.md` (the objectstack-ai#17934 落地前检③ exception, objectstack-ai#17933's per-lane table, objectstack-ai#17950's 落地前检③ line, objectstack-ai#17942's 复核归属 / 独立性件), `dispatch-runbook.md` (objectstack-ai#17942's 跨车道直接接手), `lanes/director.md` + `decision-analysis.md` (objectstack-ai#17951's director half); the objectstack-ai#17950 register / queue-guard script items are their own script PR. objectstack-ai#17934 has no edit here — its ruling lives in a deferred file. ## Rulings landed (verbatim, one commit per card) - **objectstack-ai#17942** — 5651976160 「同意」 to `1A(+三类表)·2(1)·3A·4B`; 5652079343 「以上接受你的建议。」; 5652544529 ruling ③ 「同意」 (「一律经过分诊 … 唯一例外:在飞卡的衍生子卡 … 立卡者不查重,只在卡面附 3–5 个查重词 … 查重缓存住在分诊席自己的容器里」). Q3 withdrawn — no self-routing text had reached SKILL.md (`git grep -E '自路由|自定级|self-rout' origin/main -- .claude/skills/pm-dispatch` exits 1; control 「分诊座位唯一」 hits), so nothing came out. The 「分诊不重指派」 wording does not exist in the tree (git grep exits 1) — nothing to narrow. - **objectstack-ai#17950** — 「我点头」 to the boundary 「规则层(你确认)… 事实层(席内契约复审档复核后入队):只有 `.claude/skills/pm-dispatch/references/**`」. - **objectstack-ai#17971** — 5652306063 「C. approve 后不管后续改动都由席位落地:」. - **objectstack-ai#17933** — the maintainer's misreading 「不是说你的 ui 车道不需要契约复审了吗?为什么还有 `needs:contract-review`」; `lanes/ui.md` :38 now states the applicability. - **objectstack-ai#17742** — self-triage 5642823259 (fourth disjunct if the population is small); count posted on the card first (5653074219): `pm:queue` ∧ `domain:*` ∧ ¬`priority:*` = 7 objectstack / 31 objectui at 2026-09-13T11:46Z. - **objectstack-ai#17951** — 「③ 受管面手合本身也是一条队列。能否简化我的审核步骤,比如我召唤项目总监时可以批量决定?」 — SKILL.md director line paid (+1 inside the 812), AGENTS.md PD objectstack-ai#14 clause. - **objectstack-ai#17624** — 5650203410 「同意」 to B; precondition measured: `lanes/triage.md` :7 differed from the SKILL.md line only by 「分诊」 inserted and 「裁定」 dropped — no rule lost. ## Acceptance greps (`git grep -c -F` at `BASE` → this head; every file's control still hits) | card | 0 → 1 phrase | file | control (1 → 1) | |:--|:--|:--|:--| | objectstack-ai#17942 | 「认领即跟到 MERGED」, 「分诊座位唯一生产」, 「在飞卡衍生三分」; 「简单阻塞项」 3 → 0 | SKILL.md | 「每个方案必须沿四条固定评估轴分析」 | | objectstack-ai#17950 | 「受管面两层」 / `Landing is tiered` / 「两层分档」 | SKILL.md / AGENTS.md / lanes/skills.md | `GOVERNED_SURFACES` / 「QA 波次由维护者手动触发」 | | objectstack-ai#17971 | 「席位落地」 0 → 3 (SKILL) and 0 → 1 (core-rules); `landed by the owning seat` | SKILL.md, core-rules.md, AGENTS.md | 「一座位一车道双射」 | | objectstack-ai#17933 | 「条款②复核本席适用」; 「不凭记忆或继承的注记」 1 → 0 | lanes/ui.md | 「零读数恒配点亮的正控」 | | objectstack-ai#17742 | 「析取 ④」, 「未定级数另计析取④」; core 「队列卡缺域或缺定级」 | SKILL.md, core-rules.md | 「分诊座位唯一」 | | objectstack-ai#17951 | 「受管草稿呈为一批」; `director seat requests as ONE` | SKILL.md, AGENTS.md | `A version release is performed by the maintainer` | | objectstack-ai#17624 | 「六态属他席」; 「分诊不挂」 1 → 0 | lanes/triage.md | 「饥饿守卫」 | ## Line budgets (net 0 per file, paid by density inside the file — payments named in each commit) SKILL.md 812 → 812 (widest table row 342 B, untouched); core-rules.md 151 → 151; AGENTS.md 1075 → 1075 (widest row 768 B, untouched; the `node -e` helper line under PD objectstack-ai#14 is the one deletion that is not a rule); lanes/ui.md 38 → 38; lanes/triage.md 7 → 7; lanes/skills.md 33 → 33. Every added line ≤ 120 B (`git diff BASE..HEAD | grep '^+' | awk 'length>120'` prints nothing); the whole-file `awk 'length($0)>120'` baseline is 23 / 23 on SKILL.md and 15 → 14 on AGENTS.md — all table rows or the dropped helper line, the shape the ratchet exempts (the PM's "empty over edited files" expectation is false on `origin/main` already). Decision frame block byte-identical: md5 `3327d02c56f8a0eca88569dad2270f32` (now at :733–:754). ## Deviations from the dispatch, stated - P1 「:454 leave」 falsified: the ruling lifts the S-level cap that :454 stated; leaving it would keep SKILL.md contradicting Q2 class 2 while the references PR cannot touch SKILL.md and stay seat-landable — the four 简单阻塞项 lines became the three-class rule (commit 1). - 5652544529's 「lanes/triage.md gains the cache line」: triage.md is 7/7 and its only spendable line went to ruling B — the cache rule lands in SKILL.md 〈分诊座位职责〉 instead. - The `references/**` spelling in prose: `check:pm-governed-prose` reds any `**`-shaped code span outside the register, so the tier is spelled `.claude/skills/pm-dispatch/references/` (AGENTS.md) and 本技能 `references/` (SKILL.md). - SKILL.md 〈复核〉 had a carve-out letting `.claude/` hooks/workflows/settings PRs self-land on the skills seat's review; the objectstack-ai#17950 ruling names hooks and settings in the rules layer, so those three lines became the tiering lines (provenance beyond the shallow window not read). - Table rows left untouched (widest-row pins): 状态模型 rows still list 「跨车道移交」 / 「跨域 PR 指定车道」; the bullet lines govern. ## Gates (derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on the merged head `269933d9`; all 18 run in the foreground, exit codes captured before any pipe, `--ran` reconciled: 18 derived, 18 run, 0 unrun) `check:pm-skill-ratchet` ✓ (SKILL.md 812/812 · core-rules 151/151 · AGENTS.md 1075/1075 · ui 38/38 · triage 7/7 · skills 33/33; both widest-row pins at headroom 0) · `check:pm-skill-id-lint` ✓ 27 files clean · `check:skill-frame-sync` ✓ · `check:pm-governed-prose` ✓ (5 register surfaces named, no over-claim) · `check:pm-governed-merges` ✓ · `check:nul-bytes` ✓ 8593 files · `check:refd-timer-probe` ✓ · `check:doc-authoring` ✓ · `check:required-contexts` ✓ · `check:watch-hint-literal` ✓ · `check:agent-test-spelling` ✓ · `check:docs-audit-scope` ✓ · `check:driver-memory-census` ✓ · `check-closing-keyword-parity` (+ self-test) ✓ · `check-comment-mask-corpus` ✓ · `check-governed-queue-guard --self-test` ✓ 183 cases · `@objectstack/lint check:doc-formula-expressions` ✓ (first run exit 3 PREREQUISITE NOT MET — formula/lint unbuilt; built under the verify lock, 2m52s, then ✓). No package is touched ⇒ no build/test closure owed; changeset: `.claude/**` and `AGENTS.md` publish nothing ⇒ `skip-changeset`. ## Acceptance notes (noted, not filed; 承接者 named) - `.claude/agents/os-dev.md` :50 「先搜再立」 still asks the dev to dedupe before filing; ruling ③ moves dedupe to triage (filer attaches keywords). 承接者: the skills seat — a class-1 sub-issue of objectstack-ai#17942 (os-dev.md has its own ceiling; not in this PR's file surface). - `lanes/ui.md` :25 and objectui `AGENTS.md` §9 still read 「停在 draft 等人合」 — ruling C applies to objectui governed PRs too (the card names objectui#9374 / objectstack-ai#9377). 承接者: the skills seat, sibling-repo PR. - `SKILL.md` :113 `pm:retriage` row and :106 assignee row (table rows) still name 改路由 / 跨车道移交 without the pre-dispatch qualifier; the bullet 「`pm:retriage` 改路由只对未派发卡」 governs. 承接者: whoever next edits those rows under the 342 B pin. ## 维护者速读(草稿) **改了什么**:把这周你在聊天里定下的八条裁决写进 PM 章程的规则层——认领的卡跟到合并(碰到 spec 面借复核不换席)、衍生子卡与阻塞项的三分表、定级/路由/查重收回分诊席(立卡者只附查重词)、受管面分两层(只有 pm-dispatch 的 references 目录经席内复核后入队,其余仍由你确认)、你批准之后由席位自己落地(后续推送也是)、ui 车道条款②适用面写明、分诊 sweep 补第四态、总监席把待你确认的受管草稿一批呈报、分诊席「并发出现的六态属他席」。SKILL.md、核心条款、AGENTS.md 第 14 条、三个车道文件,行数全部不变,每处新增都在同文件删重付账。 **为什么改**:每条都是已裁未落地的章程滞后;裁决原文逐字引在各 commit 与上文。 **风险与代价(含回滚)**:不发布任何包、无 changeset;规则层文本改动,回滚 = revert 本 PR。风险在两处判断:AGENTS.md 第 14 条为付行数删掉了 `node -e` 打印受管面的辅助命令(注册表所在文件仍点名);〈复核〉里「`.claude/` hooks/settings 纯代码面由 skills 席自审直接落地」的旧例外被你 09-13 的分层裁决覆盖,已删。 **未含**:references 层的对应条文(contract-review.md 的落地前检③例外与逐车道适用表、dispatch-runbook.md 的接手细则、director.md 与 decision-analysis.md 的总监批呈)另起一个 references PR,待 PR objectstack-ai#17990 / objectstack-ai#17992 落地后由席位自落;objectstack-ai#17950 的注册表与队列守卫脚本项另起脚本 PR。 **一句问**:同意「hooks/settings 的旧自审直落例外」随分层裁决一并删除吗(是/否)? --- _Generated by [Claude Code](https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…identifier — AGENTS.md's comment rule (objectstack-ai#18060) (objectstack-ai#18087) Fixes objectstack-ai#18060 A review of record is a GitHub **comment**, and `AGENTS.md` is unqualified about that surface: > …the pre-push hook refuses a model identifier in that pair; **no model identifier lands in a PR title or body, a comment, a changeset, a doc or a code comment.** The `Served-tier:` top line the in-seat contract-review protocol mandates carried the **value** of `CONTRACT_REVIEW_TIER`, which is a literal model identifier. So a record could clear a carrier **only** by putting that identifier into the very artifact the rule names. This PR makes the identifier-free spelling the **only** one: the line's token is the constant's **NAME**. Nothing evidential is traded away — the line was never the reading. The protocol already says 「⛔ 自述档位与传参皆非读数」, and the authoritative control is the seat's own transcript grep against the constant's value, which produces **no repository artifact at all**. Direction was ruled by triage (comment 5656662371, quoted verbatim, untranslated): > ### ⭐ Direction ruled — **the convention yields to `AGENTS.md`.** ⛔ Not a decision card. > ⛔ Triage rules the direction; the exact replacement wording is the implementer's. ## Premise readings All four checked against `origin/main` at `57343f761`, in the worktree, on 2026-09-14. | # | premise | verdict | evidence | |:--|:--|:--|:--| | P1 | both readers compare the token to the constant's **VALUE**, which is a literal model identifier ⇒ a record can pass today only by carrying that identifier in a comment | **holds** | `servedTierStands()` read `served.value === CONTRACT_REVIEW_TIER`; `check-governed-queue-guard.mjs` imports that very predicate through `loadRecordRecognisers`, so both gates answered from one comparison (00:10Z) | | P2 | `contract-review.md` :29 and :56 require that value | **holds** | :29 read 「值取转录 harness `model` 盖章」 and :56 「裁决 `Served-tier:` ≠ `CONTRACT_REVIEW_TIER` ⇒ exit 4」 (00:08Z) | | P3 | `git grep -n Served-tier origin/main -- .claude AGENTS.md skills` finds ONLY those two lines | **holds** | exactly 2 hits, both `contract-review.md`; the whole-tree grep adds only the two script files, 26 + 14 hits (00:09Z) | | P4 | the card's 「no gate reads it *yet*」 is **already false** on `origin/main` | **FALSIFIED — the window the card names has closed** | `273a66501` (2026-09-13T13:12Z, objectstack-ai#17990) and `60b99552a` (2026-09-13T15:00Z, objectstack-ai#18036) are both ancestors of `origin/main` — `git merge-base --is-ancestor` exit **0** for each, the self-proving direction that needs no control leg (00:41Z) | P4 is why the **readers change too** rather than only the prose. The card was filed while this was a habit; it is a gate now, and 「a gate is much harder to walk back than a habit」. ## What changed 1. **`references/contract-review.md` :29 and :56** — equal-line edits, file still 60 lines, both lines inside the 120-byte CJK prose budget (110 B and 120 B). - :29 — 「值写常量名 `CONTRACT_REVIEW_TIER`,可前置 N/N;无此行不成裁决。」 - :56 — 「`Served-tier:` ≠ 常量名 ⇒ exit 4,点名 PR、评论、读数;型号串按 `AGENTS.md` 拒。」 - The evidence stays exactly where :53 already puts it — the seat's transcript grep, which lands no artifact. 2. **`check-clause2-carriers.mjs`** — new `CONTRACT_REVIEW_TIER_NAME` is the one accepted token; new `isModelIdentifierToken()` refuses the constant's value **and** the id shape (the word claude, a hyphen, a model word — a shape, never a list, so a model nobody has named yet binds). C7's remedy quotes the **new** rule lines and names `AGENTS.md`'s rule, and ⛔ never quotes an identifier token back — a refusal that echoed it would land the identifier in one more artifact. 3. **`check-governed-queue-guard.mjs`** — its references-tier record reader takes the predicate through the same lazy recogniser import (fail-closed on a rename: a missing export is `available: false`), carries the flag on the record so the renderer never re-decides it, and its merge-queue refusal prints no identifier either. Remedy 3 now spells the token as the NAME. ⛔ **Neither gate is weakened.** The line is still required, a missing line is still a refusal, the comparison is still EXACT — no family match, no prefix floor — and the accepted token is still exactly one. The only behavioural delta is *which* single token, plus one **new** refusal class. ## Tests Self-test batteries, on `57132927c`. ⛔ No case deleted — 15 cases were re-spelled and the rest are additions: | battery | before | after | delta | |:--|--:|--:|--:| | `check-clause2-carriers --self-test` | 598 | **605** | +7 | | `check-governed-queue-guard --self-test` | 229 | **233** | +4 | The 598 baseline was measured by running `origin/main`'s own copy of the file in this tree. The 229 is derived (4 added `assert(` calls, 0 removed), because that file's copy cannot be run against a modified sibling — its fixtures are the thing this PR changes. New cases include: the constant's VALUE is refused; the refusal never quotes it back; the refusal names `AGENTS.md`'s rule; a never-shipped id binds too (a SHAPE, not a list); the two refusals differ exactly on quoting; and the row is not widened. **Gate sweep** — `node scripts/pm/dispatch-gates.mjs --commands` derived 43 families from the three changed paths; all 43 run, recorded with `--ran`: ``` ✓ dispatch-gates --ran: 43 derived famil(ies) accounted for — 43 run, 0 NOT-MEASURED (a DERIVED zero — all 43 recorded an exit code and none of them is 3). ``` `pnpm --filter @objectstack/lint run check:doc-formula-expressions` first returned **exit 3 — PREREQUISITE NOT MET** (unbuilt `@objectstack/formula` / `@objectstack/lint`, ⛔ not a finding); after `turbo run build` for those two packages under the shared verify lock it returned exit 0. **Lint** — the full repository run, not a narrowed union: `pnpm exec eslint . --no-inline-config --format json` over **6741 files**, **0 errors, 0 warnings**, exit 0. ## Legacy count — a reading for the seat, ⛔ not a work item here `origin/board-archive` at `bd7bbf53b` (snapshot 2026-09-13T20:23Z), over the archived comment bodies: | probe | count | |:--|--:| | `Served-tier` | **0** | | lit control — `Reviewed-by` | 88 | | lit control — `CONTRACT_REVIEW_TIER` | 303 |⚠️ **Read this zero narrowly.** Two corrections to how it was specified: - The prescribed path `-- archive/` does not exist on that branch — a grep there returns 0 for the wrong reason, with the lit control ALSO reading 0. The archive lives under `board/` (11,593 files). The table above is the `board/` reading, with the instrument lit. - The archive's highest card is **objectstack-ai#17600**, while the `Served-tier:` rule landed with **objectstack-ai#17990** on 2026-09-13. ⇒ the archive's window **predates the convention entirely**. The zero says the archive does not reach the window, ⛔ **NOT** that no live comment carries the line. The card's 「how many comments across the fleet carry the line」 stays unmeasured, and this PR migrates nothing. ## Landing note This PR's own governed surface is `.claude/skills/pm-dispatch/references/contract-review.md` — `check-governed-merges.mjs --test` confirms it hits the register, so the PR is **draft-only and human-merged**, or lands on the references tier's review of record.⚠️ **The review of record on THIS PR must use the NEW form** — `Served-tier:` naming `CONTRACT_REVIEW_TIER`, ⛔ not its value — because the merge-group leg runs **this PR's own guard**. A record written in the old spelling is refused by the code this PR ships. ## Acceptance notes **Open question surfaced by triage, ⛔ NOT decided here, and ⛔ `AGENTS.md` untouched.** Triage named a class the rule as written does not distinguish: **a preserved verbatim maintainer ruling that happens to contain an identifier** vs. **an identifier a seat emits as its own artifact**. This PR only removes the second kind. A blanket cleanup that rewrote preserved maintainer quotes would be worse than the problem (座位制度原话照抄不译). Whoever reconciles `AGENTS.md` should decide that explicitly. Out of scope, noted and not filed: - **238** occurrences of the constant's literal value already sit in archived comment bodies under `board/`. That is a pre-existing fact about historical compliance with the `AGENTS.md` rule, not about `Served-tier:` (which reads 0 there) — and no migration rides on this PR. - The C7 battery feeds the refusal an id **nobody has shipped** to prove it binds a SHAPE. Spelling it lands no identifier because it identifies no model — the same device `check-commit-card-trailers.mjs`'s own battery uses. A comment beside the case now says so. - `scripts/pm/dispatch-gates.mjs` is untouched: it is a `Restart-touch:` trigger file of hold objectstack-ai#14290, and the fix did not need it. The constant keeps its single value site there. No changeset: the diff publishes nothing from any released package — `.claude/**` and `scripts/pm/**` are on the fast track, and no path in the diff appears in any package's `files[]`. `Clause-②: no` ## 维护者速读(草稿) **改了什么。** 契约复核记录里那行 `Served-tier:`,原先要求写模型档位常量的**值**(一串型号标识);现在改成写常量的**名字**。两个门禁(`check-clause2-carriers.mjs` C7、`check-governed-queue-guard.mjs` 的 references 档记录读取器)同步改判,并新增一条拒绝:凡写成型号串的一律拒,且拒绝文案**不回显**那个串。 **为什么改。** `AGENTS.md` 明写「no model identifier lands in … **a comment** …」,而复核记录就是一条 GitHub 评论。优先序 `AGENTS.md` > 座位惯例,分诊已定向。关键是这笔交易**零成本**:协议本身就说自述档位不是读数,真凭据是座位的转录 grep,那个动作不落任何仓库产物 —— 所以删掉型号串不损失任何证据价值。 **风险与代价(含回滚)。** 风险低但有一个真实的过渡成本:**本 PR 自己的复核记录必须用新拼写**,因为 merge-group 那条腿跑的就是本 PR 带的守卫;用旧拼写写的记录会被拒。存量迁移不在本 PR(板存档读数 0,但那份存档的窗口早于本约定,所以「舰队里有多少条评论带旧拼写」仍未测)。回滚 = revert 两个 commit;门禁与规则文本同笔回到旧拼写,无数据迁移、无发布面。两侧门禁都**只收紧不放宽**:该行仍必填,缺行仍是拒绝。 **席位意见。** **你要做的。** 确认「预留原文维护者裁决里出现的型号串」这一边界情形该怎么定 —— 本 PR 只清理座位自己产出的那一类,⛔ 没有动 `AGENTS.md`,也⛔ 没有改写任何被原样保留的裁决引文。 --- _Generated by [Claude Code](https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #17915
CONTRACT_REVIEW_TIER's own docblock declares the comparison against the served tier EXACT — "never a family or prefix floor" — and nothing in this tree performed it. The dispatching seat passes a model as a dispatch parameter, and a passed parameter is configuration rather than a reading; the docblock's other half said the re-review sub-round's "opening self-check reads this", but a self-check is prose to the reviewer, so a round that simply did not run it produced a verdict indistinguishable from one that did. The census on the card measured 11 rounds served below the declared tier across four days and eleven PRs, five of them the only clearance a mergedClause-②: yespair ever had.This lands items 1–3 of the director ruling: the verdict carries the reading, and the strip is gated on it.
What changed
.claude/skills/pm-dispatch/references/contract-review.md(2 lines of new rule, net 0 at the ratchet):scripts/pm/check-clause2-carriers.mjs— a new finding row, C7:readServedTierreads aServed-tier:key line withReviewed-by:'s own discipline (case-sensitive key, the markdown decoration a seat writes without meaning it). Three-valued:read/unreadable/missing— a carrier never started and one started and left unreadable are different facts.[N/M ]tier, corrected from a fixture against the live board before shipping. Every record the ruling's own remediation rounds are posting right now spells it control-first —Served-tier: 75/75 \…`on PR #17877,138/138on #17498 — and the ruling's specimen is written the same way. A reader that demanded the tier token immediately after the colon would have refused every verdict produced under the rule it enforces, on day one. TheN/M` is judged rather than skipped: it is the zero-hit control the discipline already requires, and a count that is not total is the 「回退证据」 whose own rule text voids a verdict entire. Absent, it is vacuous — the tier alone decides, which is the ruling's minimum, so nothing the ruling permits is refused.reviewOfRecordcarries the reading on the record it already chose, so C6 and C7 can never disagree about which verdict a clear stands on.c7ServedTierBelowfires on C6's population and no other —needsRecordRead's completed state, i.e. a clearance judgment of a hung carrier: declaredyes, the gate bound and cleared on both carriers, head unmoved. The refusal names the PR, the verdict comment and the served value, at exit 4 (a limb not standing), never 3.CONTRACT_REVIEW_TIER, imported fromdispatch-gates.mjsso the model id keeps exactly one value site acrossscripts/pm/**and.claude/skills/pm-dispatch/**. No model identifier appears anywhere in this diff outside that import.0/0void,12/133refused as fallback evidence, absent vacuous, a perfect control never rescuing a below-tier value), the exactness pins (a family prefix and an extended value both refused), and the four populations the row must never reach.Deliberately NOT in scope
A
Clause-②: nopair that never carried the label is never refused for lacking the line — it is not in the candidate shape, so the row cannot reach it. A pair still carrying the gate owes nothing yet. An absent or unsigned record stays C6's row alone. No PASS/FAIL token is read to reach any of it: what produced a verdict is measurable, what it concluded stays human.One deviation from the dispatched file surface — and why it is inside it
The dispatch scoped the diff to those two files. It is those two files — but one edit inside the checker was not foreseen and is worth reading before approving.
scripts/pm/check-clause2-carriers.mjscarried adispatch-gates: no-path-populationmarker: "this gate reads no file in the tree at all … so no card's file surface can predict it". The input half is still exactly true. The other half stopped being true the moment C7's import landed: a card editingCONTRACT_REVIEW_TIERmoves the value every clearance is judged against, so it does predict this gate.pnpm check:pm-dispatch-gatescatches this directly — its live-half caseno family both DECLARES no path population and names paths anywaywent red oncheck:pm-clause2-carriers, measured by ablation (base tree: hints[]; with the import: hints[".github/workflows"]).Keeping a declaration that stopped being true is the exact shape C7 itself exists against, so the marker is retired, and the comment left in its place states the trade rather than hiding it: the import channel contributes a followed module's own literals, so this family now also inherits a
.github/workflowslead it never opens, on a gate whose CI step runs the self-test only. The designed narrowing (inherited-population, declared by the followed module) cannot express this case — it is per-module, and the same module's globs are a real population forcheck:pm-widening-tells, which reads them. Filed separately as #17991 rather than worked around here.⛔ The alternative — restating the tier in this file — is the thing that let the declared tier and the served one drift apart in the first place, and is refused.
Acceptance measurements
All taken at
fed29ced, against base9ccc4179.Served-tierinreferences/contract-review.mdServed-tierincheck-clause2-carriers.mjsReviewed-byinreferences/contract-review.md(lit control)references/contract-review.mdlinescheck-clause2-carriers.mjslongest line (bytes)references/contract-review.mdis ≤ 120 bytes (LC_ALL=C awk 'length($0)>120'prints nothing); the four edited lines measure 105 / 113 / 111 / 108 B. The 120-byte register is the reference file's; the checker keeps its own line style unchanged (471 → 502 lines over 120 B, max unmoved at 390).git diff --stat origin/main...HEAD→ exactly.claude/skills/pm-dispatch/references/contract-review.mdandscripts/pm/check-clause2-carriers.mjs.+1the 同形 line,+1the mechanism line replacing the 转录档位核验 prose,-1by folding 「⛔ 自述档位不是读数」 and 「传参只是配置 ⛔ 不作达档读数」 into one clause (they are one rule: a tier claim that is not a harness stamp is not a reading),-1by dropping the 转录核验 grep recipe, whose method survives at :49 (「每场前必读服役档,读法见platform-readings.md」) and in the new 同形 line.node scripts/pm/check-skill-line-ratchet.mjsgreen at 60/60, headroom 0.Checks
pnpm check:pm-clause2-carriers— 588 cases pass (546 before this PR; the C7 battery is 42 and is registered with its own floor,SELF_TEST_BATTERY_FLOOR19 → 20, preserving the roster's existing slack).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) on the merged head: 41 families, 41 run, 41 exit 0. Reconciled:--ranreports41 derived famil(ies) accounted for — 41 run, 0 NOT-MEASURED (a DERIVED zero — all 41 recorded an exit code and none of them is 3).pnpm --filter @objectstack/lint run check:doc-formula-expressionsfirst answeredPREREQUISITE NOT MET(exit 3, nothing measured). Built its two declared prerequisites under the shared verify lock (VERDICT command-exit 0 · held the lock 141s) and re-ran it: exit 0.Served-tier:requirement turned the reference record fixture red —⭐ the #14155 specimen WITH its record still reads CLEAN overallfailed with["C7"]— before the fixture gained the line. The row can fail.node scripts/pm/check-clause2-carriers.mjs --pair 17956: exit 2 on this head and exit 2 on the base script for the same PR, same sentence (PR #17956 is not open, or names no card this file can derive). The pair could not be formed, so nothing about it was judged — the dispatch expected 0 for aClause-②: nopair, and that PR has since left the open set. ⛔ Not a C7 refusal, and unmoved by this diff.eslint .over oneeslint.config.mjs;npx eslint --no-inline-config --format json scripts/pm/check-clause2-carriers.mjs→ 1 file, 0 errors, 0 warnings; the narrowing excludes nothing because that config "never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file" (its own line 328), so this diff cannot move any untouched file's verdict. The.mdis not an eslint input.grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over both edited files: no hits;pnpm check:nul-bytesgreen.Changeset
skip-changeset—scripts/pm/**and.claude/**publish nothing: neither path is in any package'sfiles[], and both are on the fast track (.claude/**and PM tooling). Label applied and read back.Acceptance notes
inherited-populationis per-module, so no caller can decline a fabricated lead #17991 — the import channel's over-reach, with both readings:inherited-populationis keyed on the followed MODULE while fabrication is a property of the CALLER (the same module's globs are a real population forcheck:pm-widening-tells, which reads them), and theif (entry.selfTest) continue;guard built to stop exactly this inheritance never fires for apnpm check:*family, becauseselfTestis read off the workflow argv while the--self-testlives in thepackage.jsonscript body. Searched first: [finding] dispatch-gates.mjs is a followable non-gate module, so its join bases and tier globs are inheritable — 2553 fabricated pairs for the next gate that imports it #11556 (already resolved, and left alone here) is the same class by a route its remedy cannot express; no open card covers it.维护者速读(草稿)
改了什么 — 契约复核裁决从此必须带一行
Served-tier:,值取复核者转录里 harness 逐消息盖的model字段;check-clause2-carriers.mjs --pair在判定「双载体已清」时读这一行,不等于CONTRACT_REVIEW_TIER就拒(exit 4),并点名 PR、裁决评论和读到的档位。规则文本同步落在references/contract-review.md,行数 60 → 60。为什么改 — 常量自己的 docblock 写着「与服役档的比较是 EXACT」,而树上没有任何东西在比。派进去的 model 是配置不是读数,「开场自检」是写给复核者的散文:一轮不跑它,产出的裁决与跑了的长得一模一样。卡上实测 11 轮在档下产出裁决,其中 5 轮是已合并
Clause-②: yesPR 唯一的清标依据。这是本仓在别处一律拒绝的 declared ≠ enforced,落在「一次公共契约加宽到底有没有被复核过」那道门本身。风险与代价(含回滚) — 失败方向是响亮的:清标被拒,不是被静默放行。代价一:规则落地前写的历史裁决没有这一行,再被判定时会红,补救是复核席把自己转录里已经盖好的读数补写成一条新记录(最新的记录优先,不动载体)。代价二:本 PR 让这个门禁第一次有了树内依赖(
CONTRACT_REVIEW_TIER所在文件),因此退掉了它「无路径面」的旧声明;派生因此多送一条.github/workflows的线索,是噪音、已在文件里写明,并已记入验收备注。回滚 = revert 本 PR,一次 revert 即可,门禁回到今天的状态。席位意见 — (留空,待席位定稿)
你要做的 — 受管面(
.claude/**),本 PR 恒为 draft,⛔ 不由任何 AI 席位合并、入队或挂 auto-merge。请人工确认两件事:① 规则文本那两行的措辞;② 退掉no-path-population声明这一步是否接受(替代方案是把常量在本文件再写一遍,那正是让档位漂移的那个形状,已拒)。Provenance
Authored by the
domain:skillsseat's dispatched executor, sessionsession_01DAcomhvR9kKizeYgg89Vo8(https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8), on branchclaude/issue-17915-served-tier-gates-the-strip. Attribution is stated here in prose because a REST edit of a PR body appends its own footer block: the first edit of this body left two, and this revision sends none so the appended one is the only one.Generated by Claude Code