fix(pm): the Served-tier line names the tier constant, never a model identifier — AGENTS.md's comment rule (#18060) - #18087
Conversation
…identifier A review of record is a GitHub comment, and AGENTS.md is unqualified about that surface: no model identifier lands in a PR title or body, a comment, a changeset, a doc or a code comment. The Served-tier: line required by the in-seat contract-review protocol carried the constant's VALUE, so a record could clear a carrier ONLY by putting an identifier into the very artifact that rule names -- and two gates had already landed readers that compare against that value, so the convention was on its way from a habit into a gate, which is much harder to walk back. So the accepted token is now the constant's NAME. Nothing evidential is traded away: the line was never the reading. The protocol already says a self-declared tier and a passed parameter are both non-readings, and the authoritative control is the seat's own transcript grep against the constant's value -- a grep that produces no repository artifact at all. - references/contract-review.md: the two rule lines spell the token as the constant's name and name the refusal, at equal line count. - check-clause2-carriers.mjs: C7 accepts the NAME; a token of identifier shape -- the old spelling included -- is refused with a remedy that names AGENTS.md's rule and never quotes the token back, because a refusal that quotes it lands the identifier in one more artifact. - check-governed-queue-guard.mjs: the references-tier record reader imports that predicate (fail-closed on a rename) and carries the flag to its renderer, so the merge-queue refusal never prints one either. Neither gate is widened: the line is still required, a missing one is still a refusal, and the accepted token is still exactly one. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
The C7 battery proves the refusal binds a SHAPE by feeding it an id nobody has shipped. Spelling it is safe precisely because it identifies no model, and that is the same device the pre-push trailer rule's own battery uses -- worth one comment, so a later reader greps `claude-` and finds the answer beside the case instead of filing it. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
Verification on the final head —
|
| battery | mutated | restored |
|---|---|---|
check-clause2-carriers --self-test |
exit 1 — the three new #18060 cases failed by name | exit 0, 605 pass |
check-governed-queue-guard --self-test |
exit 1 — 2 of 233 failed, the record reading state: "stands" with servedIsIdentifier: true |
exit 0, 233 pass |
⇒ the refusal is load-bearing in both files, and the guard leg fails through the imported predicate rather than a second parser of its own.
Restore proof: the target was checked out by naming HEAD explicitly — ⛔ never the bare two-dash form, which restores from the polluted index and exits 0 having undone nothing — then its blob hash compared equal to the HEAD blob and git diff HEAD read empty. The script armed a restore trap on EXIT, INT and TERM with an absolute repo-root path throughout, and treats an empty hash as a FAILURE rather than as nothing to compare.
⛔ No permanent artifact: the ablation is a one-shot proof, cited here and in the report, and no file from it is left in the tree.
Generated by Claude Code
Contract reviewHead: ① derived judgments (seat-measured on the fetched head, ⛔ not taken from the report):
② semver: unchanged — nothing published. ③ boundary flags: (a) the preserved-verbatim-quotation class — AGENTS.md's comment rule says 「a comment」 without qualification, while seat posts carry maintainer rulings that contain identifiers under 原话照抄不译 — surfaced by triage and the dev, ⛔ not decided here; this seat files it as a decision card (option A recommended: carve out preserved quotations in AGENTS.md). (b) Legacy: Implemented-by: Verdict: PASS — ready + auto-merge by this seat. Generated by Claude Code |
Fixes #18060
A review of record is a GitHub comment, and
AGENTS.mdis unqualified about that surface:The
Served-tier:top line the in-seat contract-review protocol mandates carried the value ofCONTRACT_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):
Premise readings
All four checked against
origin/mainat57343f761, in the worktree, on 2026-09-14.servedTierStands()readserved.value === CONTRACT_REVIEW_TIER;check-governed-queue-guard.mjsimports that very predicate throughloadRecordRecognisers, so both gates answered from one comparison (00:10Z)contract-review.md:29 and :56 require that valuemodel盖章」 and :56 「裁决Served-tier:≠CONTRACT_REVIEW_TIER⇒ exit 4」 (00:08Z)git grep -n Served-tier origin/main -- .claude AGENTS.md skillsfinds ONLY those two linescontract-review.md; the whole-tree grep adds only the two script files, 26 + 14 hits (00:09Z)origin/main273a66501(2026-09-13T13:12Z, #17990) and60b99552a(2026-09-13T15:00Z, #18036) are both ancestors oforigin/main—git merge-base --is-ancestorexit 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
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).CONTRACT_REVIEW_TIER,可前置 N/N;无此行不成裁决。」Served-tier:≠ 常量名 ⇒ exit 4,点名 PR、评论、读数;型号串按AGENTS.md拒。」check-clause2-carriers.mjs— newCONTRACT_REVIEW_TIER_NAMEis the one accepted token; newisModelIdentifierToken()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 namesAGENTS.md's rule, and ⛔ never quotes an identifier token back — a refusal that echoed it would land the identifier in one more artifact.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 isavailable: 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:check-clause2-carriers --self-testcheck-governed-queue-guard --self-testThe 598 baseline was measured by running
origin/main's own copy of the file in this tree. The 229 is derived (4 addedassert(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 --commandsderived 43 families from the three changed paths; all 43 run, recorded with--ran:pnpm --filter @objectstack/lint run check:doc-formula-expressionsfirst returned exit 3 — PREREQUISITE NOT MET (unbuilt@objectstack/formula/@objectstack/lint, ⛔ not a finding); afterturbo run buildfor 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 jsonover 6741 files, 0 errors, 0 warnings, exit 0.Legacy count — a reading for the seat, ⛔ not a work item here
origin/board-archiveatbd7bbf53b(snapshot 2026-09-13T20:23Z), over the archived comment bodies:Served-tierReviewed-byCONTRACT_REVIEW_TIER-- 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 underboard/(11,593 files). The table above is theboard/reading, with the instrument lit.properties.visibleloses its CEL envelope and is evaluated on the LEGACY JS engine —has()faults and the fail-soft default renders the banner on every row #17600, while theServed-tier:rule landed with feat(pm): gate the clause-② carrier strip on the served tier the verdict declares #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 --testconfirms it hits the register, so the PR is draft-only and human-merged, or lands on the references tier's review of record.Served-tier:namingCONTRACT_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.mduntouched. 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 reconcilesAGENTS.mdshould decide that explicitly.Out of scope, noted and not filed:
board/. That is a pre-existing fact about historical compliance with theAGENTS.mdrule, not aboutServed-tier:(which reads 0 there) — and no migration rides on this PR.check-commit-card-trailers.mjs's own battery uses. A comment beside the case now says so.scripts/pm/dispatch-gates.mjsis untouched: it is aRestart-touch:trigger file of hold dispatch-gates: STAGE-THEN-RUN reaches a program by an edge neither follow traverses — check:objectui-changeset inherits nothing from scripts/bump-objectui.sh #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/**andscripts/pm/**are on the fast track, and no path in the diff appears in any package'sfiles[].Clause-②: no维护者速读(草稿)
改了什么。 契约复核记录里那行
Served-tier:,原先要求写模型档位常量的值(一串型号标识);现在改成写常量的名字。两个门禁(check-clause2-carriers.mjsC7、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