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 |
…reserved as a quotation block (objectstack-ai#18315) Fixes objectstack-ai#18092 ## What this PR does `AGENTS.md`'s comment rule (the **Commit message:** sentence group, :440–:444) gains the one closed exemption ruled on the card, in place, at the 1075-line ceiling. No probe changes, because none reads prose for model identifiers (measured below). The diff is four lines of `AGENTS.md` and nothing else. Landing: this is the rules layer, so this PR is a **draft and stays one** until an authorized APPROVED review; the owning seat lands it afterwards. Per ruling C on objectstack-ai#17971, no seat readies, queues or arms it, and this dev requests no reviewer. `skip-changeset`: nothing published moves. ## The ruling (comment 5682890409, letter A, quoted verbatim and untranslated) > - **A** — AGENTS.md's comment rule (:442 「no model identifier lands in a PR title or body, a comment, a changeset, a doc or a code comment」) gains one closed exemption: **a verbatim maintainer ruling preserved as a quotation block** (the seat-post protocol's 「原话照抄不译」 act). The exemption names the quotation form only; a model identifier a seat writes in its own prose stays a violation (PR objectstack-ai#18087's class). > - The model-identifier probes (the H64 family and any 型号探针 audit) exclude quotation blocks so they stop counting preserved rulings. > - **B** (rewrite preserved rulings) and **C** (leave it to each seat) are refused; triage 5656662371's reading stands: a cleanup that rewrites maintainer quotes would be worse than the problem. ⛔ No existing quotation is edited. ## The sentence (before → after, `AGENTS.md` :440–:444) Before (five lines, 77 / 107 / 111 / 104 / 113 bytes): > …no model identifier lands in a PR title or body, a comment, a changeset, a doc or a code comment. The one exemption is a REPORTING one: a harness-written `Co-Authored-By` trailer is not declared a deviation; the pair stays model-free; landed history is not rewritten. After (five lines, 77 / 115 / 116 / 119 / 110 bytes; :440 byte-identical, :441–:444 reflowed within the same five lines): > …no model identifier lands in a PR title or body, a comment, a changeset, a doc or a code comment. Two exemptions: a harness-written `Co-Authored-By` trailer (REPORTING: not declared a deviation; landed history is not rewritten) and a verbatim maintainer ruling preserved as a quotation block. The exemption phrase is the ruling's own wording. The maintainer edits wording and placement at review — nothing here is polished beyond the ruling. ### Why in place, and what paid for it (the breach the dispatch asked me to stop on) The dispatch's route was "extend :443–:444 in place; a long line is legal there (14 lines already exceed 120 chars)". Measured: `pnpm check:pm-skill-ratchet` applies a 120-byte width rule to every ceilinged file, `AGENTS.md` included, and the 14 over-width lines are all table rows or the one blockquote — structurally exempt classes. A prose line over 120 bytes goes red (`lengthVerdict`, silent on pass). So the sentence had to fit five lines of at most 120 bytes each. Byte math (greedy wrap, code spans as atoms): the group's usable capacity is 77 + 4×120 minus wrap loss; every wording that also kept the clause "the pair stays model-free" and the 「原话照抄不译」 parenthetical needed a sixth line (the tightest such wording overflowed by exactly the 27-byte parenthetical). No line in the paragraph says nothing the new sentence does not, so the "paid line in the same paragraph" route had no candidate. What paid: **the deletion of the restated clause "the pair stays model-free;"** — :440 already says the commit "ends with the model-free trailer pair" and :442 says "the pre-push hook refuses a model identifier in that pair", so the clause restates the sentence group's own first sentence and its hook. Content deletion is the ratchet's only legal currency; no line was folded to buy one (line count 1075 → 1075, and the reflow of :441–:444 frees nothing). The 「原话照抄不译」 act-name is carried here and on the card rather than in the sentence; the maintainer may prefer it in the sentence, in which case the ceiling needs a ruled +1 (the ratchet's `CEILINGS` entry for `AGENTS.md` has precedent for a ruled raise) — that is the maintainer's call, not this dev's. ## The probes — measured; none reads prose for model identifiers, so none changes | candidate | what it reads | keys on identifiers? | reads prose? | |---|---|---|---| | `scripts/check-commit-card-trailers.mjs` (:848 iterates four id literals in its self-test) | the trailer BLOCK of unpushed commit messages; header: 「WHAT IS JUDGED is the trailer VALUE, and only inside the trailer block」, 「⛔ NOT the message body」 | yes — `MODEL_ID_FORM` on the trailer value | no | | `.githooks/pre-push` | invokes the gate above with the pushed refs | via the gate | no | | `scripts/check-agent-model-declared.mjs` | the YAML frontmatter `model:` key of `.claude/agents/*.md`; a `model:` mention inside description prose is pinned as NOT a declaration | validates the pin's shape | no | | `scripts/pm/check-clause2-carriers.mjs` (`isModelIdentifierToken`) | the single `Served-tier:` token of a review-of-record comment; header: 「What a seat writes AFTER the token is its argument, which this file does not read」 | yes — on that one token | no | | `scripts/pm/check-governed-queue-guard.mjs` | the same `Served-tier:` token through the clause-2 predicate | yes — same token | no | | `scripts/pm/check-half-states.mjs` H64 | a seat/dev-signed artefact that names no `session_` id (`h64SpeaksAbout` = seat signature present AND no session id) | no — session ids, not identifiers | reads bodies, but never for identifiers | | `scripts/pm/check-half-states.mjs` H65 (merged from `origin/main` at `10a7b9fa` during this PR) | a triage round artefact that names no tier | no | same | | `.claude/hooks/*`, `packages/lint/scripts/*`, `.github/workflows/*` | — | zero identifier-shaped regexes (one workflow comment mentions a pin) | no | | a 型号探针 recipe in the skill tree | grep of `.claude/skills/pm-dispatch/**` and `.claude/agents/**` for 型号 / 探针 | no such recipe exists; every 探针 hit is a REST/queue probe | — | So the ruling's probe half has no in-tree target: the seat's own ad-hoc greps were the only "型号探针", and they are not repository code. A self-test case "a quotation block carrying an identifier is not counted; a seat-prose identifier still is" has nothing to attach to without inventing a new prose scanner, which would be scope the card does not carry. ## Quotation form (measured on the protocol and the live seat posts) The protocol names the act as 裁决引文照抄不译 (`.claude/skills/pm-dispatch/SKILL.md` :100; `references/core-rules.md` :26) and `AGENTS.md` Communication says 「引用中文裁决时保持原文、不翻译」. On the live posts, every identifier-carrying preserved ruling is a 「…」 corner-bracket span inside prose labelled 原话照抄不译 / 逐字 / verbatim — objectstack-ai#6015 L128 and L130, objectstack-ai#6021 L24, L34 and L136 — and zero are Markdown blockquotes (`> …`); the 47 blockquote lines in objectstack-ai#6015 are the seat's own correction notes, objectstack-ai#6021 and objectstack-ai#7623 carry none. The ratchet's own exemption classes already recognise both shapes as verbatim maintainer rulings (`quotation` = a multi-line 「…」/『…』 ruling; `blockquote` = 「Every blockquote in this corpus is a verbatim maintainer ruling」). The sentence pins the ruling's letter, "a quotation block", and this PR pins no narrower form in code because no code reads one. ## Verification - Gates derived on the merge commit `b671f83b` by `node scripts/pm/dispatch-gates.mjs --commands` (14 families, identical to the derivation on `dc3d1c16`): `check-closing-keyword-parity` (+ `--self-test`), `check-comment-mask-corpus`, `check:agent-test-spelling`, `check:docs-audit-scope`, `check:driver-memory-census`, `check:nul-bytes`, `check:pm-governed-merges`, `check:pm-governed-prose`, `check:pm-skill-id-lint`, `check:pm-skill-ratchet`, `check:refd-timer-probe`, `check:required-contexts`, `check:watch-hint-literal` — all exit 0; `--ran` reconciliation: 「14 derived famil(ies) accounted for — 14 run, 0 NOT-MEASURED」. - `pnpm check:pm-skill-ratchet`: 「AGENTS.md is 1075 lines (ceiling 1075; headroom 0)」, no width offender; `check:pm-skill-id-lint`: 27 files clean; `node scripts/check-self-test-wired.mjs`: exit 0. The battery `check:pm-dispatch-gates` was not derived and was not run; no `.mjs` was touched, so no ESLint run is owed. - Ablation from the committed state (`dc3d1c16`, whose `AGENTS.md` blob `98e7cd4a` is the merge commit's blob too): base blob `7c7a5eee` restored with `git restore --source=BASE` (on-disk hash proven equal) → the sentence count reads 0 and the restated clause reads 1, ratchet exit 0 at 1075 lines; HEAD blob restored with `git checkout HEAD -- AGENTS.md` (hash proven equal), `git diff HEAD` and `git status --porcelain` both empty; restore trap on EXIT/INT/TERM. On HEAD the sentence count reads 1 and the restated clause reads 0. ## Acceptance notes - noted, not filed: the dispatch's premise that a long prose line is legal in `AGENTS.md` is false under the ratchet's width rule; the 14 over-width lines are table rows and one blockquote. 承接者: the skills seat's next `AGENTS.md` dispatch reads the `lengthVerdict` exemption classes before choosing "extend in place". - noted, not filed: the ruling's probe half names 「the H64 family」; H64 keys on session ids and never on identifiers, so the exclusion it asks for has no code to land in. 承接者: the director seat, if a prose scanner is ever proposed. - `origin/main` moved once under this branch (objectstack-ai#18311, `scripts/pm/check-half-states.mjs` only); merged in, never rebased; the union was re-run on the merge commit. ## 维护者速读(草稿) **改了什么**:`AGENTS.md` 的评论规则句组(:440–:444)从「唯一豁免是 REPORTING」改为「两条豁免」:harness 写入的 `Co-Authored-By` trailer(REPORTING:不申报为偏差;已落地历史不改写)与逐字保存为引用块的维护者裁决。行数 1075 → 1075,每行 ≤120 字节。 **为什么改**:你在 objectstack-ai#18092 批准了 A:座位贴按制度把你的裁决原话照抄不译,原话里若带型号,转抄不算违规;席位自己写的型号仍算。探针半边:仓库里没有任何脚本按型号扫描散文(逐一实测列在正文表格),所以没有探针要改。 **风险与代价(含回滚)**:代价是删掉了「the pair stays model-free;」这句重述(句组首句与 pre-push hook 已各说一次),以及豁免句里没放 「原话照抄不译」 这个名字(字节装不下;若你要放进去,需要你裁一次上限 +1)。回滚 = revert 这一个提交,无发布物。 **席位意见**:(留空,席位定稿) **你要做的**:审阅措辞与位置(你可直接改),若同意则给 APPROVED,由 skills 席落地;不同意「删重述句」的付账方式,则改裁「上限 +1」或指一行删除。 --- _Generated by [Claude Code](https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr)_ Co-authored-by: Claude <noreply@anthropic.com>
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