feat(pm): teach the merge-queue guard the governed surface references tier - #18036
Conversation
…es tier The maintainer tiered the governed surface on 2026-09-13 and the charter text landed with PR #18018, but the queue guard still demanded an authorized approval for every governed path — so the tier was declared and not enforced. The merge-group leg now classifies each governed pull request's governed paths and, when they ALL lie under `.claude/skills/pm-dispatch/references/`, accepts the skills seat's review of record on the CURRENT head in place of the approval: a `## Contract review` comment on the PR thread carrying a `Reviewed-by:` line and a `Served-tier:` reading that stands. Every other governed path is the rules layer and keeps today's predicate byte-for-byte — 16 predicate bodies are md5-identical to their pre-change selves. Recognition is IMPORTED, never re-implemented: the heading marker, the head-sha span test, the newest-of resolution, the `Reviewed-by:` key line and the `Served-tier:` reader all come from the two files that own them. Reaching them needed the module-eval cycle solved rather than worked around, so the import is lazy and this file's dispatch no longer carries a top-level await; the battery pins that precondition against this file's own source. The leg is MONOTONE by construction — consulted only for an entry no authorized approval satisfied — so it can lift a refusal and can never create one. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewHead: ① derived judgments — the merge-group leg gains the references tier the charter declared in PR #18018; the rules layer keeps today's predicate:
Seat measurements on the head tree ( ② semver: ③ boundary flags: Implemented-by: Verdict: PASS — the tier the charter declared is now enforced on the merge-group leg, monotone, with imported recognition and a fail-closed load. Generated by Claude Code |
…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 #18020
Part of #17950. The maintainer tiered the governed surface on 2026-09-13 (「我点头」) and the charter text landed with PR #18018, but
check-governed-queue-guard.mjsstill demanded an authorized approval for every governed path — the tier was declared, not enforced. The merge-group leg now learns it.What changed
A governed pull request whose governed paths all lie under
.claude/skills/pm-dispatch/references/is satisfied by the skills seat's review of record on the current head — a## Contract reviewcomment on the PR thread carrying aReviewed-by:line and aServed-tier:reading that stands — in place of the approval. One rules-layer path in the same diff and today's predicate is the only way through.Monotone by construction. The tier leg is consulted only for an entry no authorized approval satisfied, so it can lift a refusal and can never create one. Nothing that passes the queue today newly refuses.
Recognition is imported, never re-implemented — the heading marker, head-sha span test and newest-of resolution (
check-half-states.mjs), theReviewed-by:/Served-tier:readers (check-clause2-carriers.mjs). Zero new parsers;check-clause2-carriers.mjscarries one export-only change (const→export const REVIEWED_BY_LINE, value expression md5-identical).Acceptance greps (both directions)
REFERENCES_TIER_PREFIXin the guardreadServedTier/servedTierStandsimportedGOVERNED_APPROVERS(lit control — still hits)Two measurements the route turned on
check-clause2-carriers.mjs, which imports H31's file. So the import is lazy, which is legal only because this file's dispatch no longer carries a top-levelawait. That precondition is pinned against this file's own source; ablation D (restoringawait main()) reds exactly that one case, and ablation C (in the self-test dispatch) reproduces the exit-13 deadlock.GET /repos/{o}/{r}/issues/{n}/commentsanswersX-Accepted-GitHub-Permissions: issues=read; pull_requests=read, and GitHub documents the semicolon as separating alternative permission sets. The workflow's existingpull-requests: readis sufficient; ⛔ no workflow change.Reverse verification (mutate → prove on disk → run → restore)
entrySatisfiedaccepts any record stateawaitin the self-test dispatchawaitin the live dispatchEach leg proved its mutation on disk before running, and its restore by blob hash against
HEAD.Acceptance notes
AGENTS.mdPD feat: Comprehensive CRM example demonstrating all ObjectStack protocol features #14's sentence 「the queue guard refuses an unpinned governed diff」 stays true: a references-only PR is pinned by its record. No prose changed;check:pm-governed-proseis not in the derived set for this diff and is green when run anyway.makeLabelReader's docblock says the issues-labels route "needsissues: read, which this workflow does not grant". The live API answersissues=read; pull_requests=readfor it too, so the stated reason is stale — the choice to read the pull object is still right (it reuses a call the leg already makes). Successor: whoever next edits that reader, in this same file.Authored by the
domain:skillsseat, sessionsession_01DAcomhvR9kKizeYgg89Vo8.Gates (all at
f088df57)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 37 families; all 37 run, every one with its exit code captured before any pipe, all 0.--ranreconciles: 37 derived, 37 run, 0 NOT-MEASURED (a DERIVED zero), 0 UNRUN. Includescheck:pm-dispatch-gates(1682 self-test cases),check:pm-clause2-carriers,check:nul-bytes,check:refd-timer-probe,check:closing-target-claim,check:whole-set-label-write, and the guard's own--self-test(229 cases).check:pm-governed-proseis not in the derived set for this diff — no prose surface changed — and was run anyway: green, 2 instruction surfaces name all 5 registered governed surfaces and claim no others.eslint . --no-inline-configran the whole population rather than a narrowing: 6722 files, 0 errors, 0 warnings. NoparserOptions.projectand no typed rules are configured, so no untouched file's verdict can move with this diff.Generated by Claude Code