Skip to content

feat(pm): teach the merge-queue guard the governed surface references tier - #18036

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-18020-queue-guard-references-tier
Sep 13, 2026
Merged

os-project-manager merged 1 commit into
mainfrom
claude/issue-18020-queue-guard-references-tier

Conversation

@claude

@claude claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

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.mjs still 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 review comment on the PR thread carrying a Reviewed-by: line and a Served-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), the Reviewed-by: / Served-tier: readers (check-clause2-carriers.mjs). Zero new parsers; check-clause2-carriers.mjs carries one export-only change (constexport const REVIEWED_BY_LINE, value expression md5-identical).

Acceptance greps (both directions)

reading before after
REFERENCES_TIER_PREFIX in the guard 0 9
readServedTier / servedTierStands imported 0 / 0 4 / 4
GOVERNED_APPROVERS (lit control — still hits) 34 39
self-test cases 183 229
the tier VALUE spelled in the guard 0 0
predicate bodies md5-identical to their pre-change selves 16 of 16 untouched

Two measurements the route turned on

  1. The cycle is real, and indirect. A module-scope import of the recognisers deadlocks (node exits 13, "Detected unsettled top-level await") — measured directly and through 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-level await. That precondition is pinned against this file's own source; ablation D (restoring await main()) reds exactly that one case, and ablation C (in the self-test dispatch) reproduces the exit-13 deadlock.
  2. The thread read widens no scope. GET /repos/{o}/{r}/issues/{n}/comments answers X-Accepted-GitHub-Permissions: issues=read; pull_requests=read, and GitHub documents the semicolon as separating alternative permission sets. The workflow's existing pull-requests: read is sufficient; ⛔ no workflow change.

Reverse verification (mutate → prove on disk → run → restore)

ablation cases red of 229
A — drop the prefix's trailing slash 2
B — entrySatisfied accepts any record state 12
C — top-level await in the self-test dispatch exit 13, deadlock
D — top-level await in the live dispatch 1 (the precondition pin)

Each leg proved its mutation on disk before running, and its restore by blob hash against HEAD.

Acceptance notes

  • AGENTS.md PD 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-prose is not in the derived set for this diff and is green when run anyway.
  • Noted, not filed: makeLabelReader's docblock says the issues-labels route "needs issues: read, which this workflow does not grant". The live API answers issues=read; pull_requests=read for 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:skills seat, session session_01DAcomhvR9kKizeYgg89Vo8.

Gates (all at f088df57)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 37 families; all 37 run, every one with its exit code captured before any pipe, all 0. --ran reconciles: 37 derived, 37 run, 0 NOT-MEASURED (a DERIVED zero), 0 UNRUN. Includes check: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-prose is 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-config ran the whole population rather than a narrowing: 6722 files, 0 errors, 0 warnings. No parserOptions.project and no typed rules are configured, so no untouched file's verdict can move with this diff.


Generated by Claude Code

…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>
@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author
  • Served-tier: 1084/1084 claude-fable-5-1 — harness model stamp counted over this seat's own transcript (non-sidechain assistant messages a model served; <synthetic> harness notices excluded) at 2026-09-13T14:47Z; get_session external_metadata.last_served_model read claude-fable-5-1 at 2026-09-13T14:41Z.

Contract review

Head: f088df57 (PR #18036, card #18020) — read at 2026-09-13T14:49Z by the skills seat at the contract-review tier. NOT GOVERNED, measured: the diff touches scripts/pm/check-governed-queue-guard.mjs (+779 −28) and one export keyword in scripts/pm/check-clause2-carriers.mjs ⇒ in-seat review, then ready + auto-merge by this seat.

① derived judgments — the merge-group leg gains the references tier the charter declared in PR #18018; the rules layer keeps today's predicate:

  1. Tier (governedTierFor): ALL governed paths of the entry under .claude/skills/pm-dispatch/references/ (prefix with its trailing slash, never a glob) ⇒ references; empty or mixed ⇒ rules. The asymmetric default is the right one and the sibling-directory case is pinned in the refusing direction.
  2. Satisfaction (entrySatisfied): the approval limb first and unchanged; the tier limb only ADDS a pass, and only on a merge_group entry whose record key exists. Monotone, measured with throwing spies (an approved references PR reads no thread and loads no recogniser); the pull_request leg's rendering is byte-identical by construction.
  3. The record (recordVerdict): five-valued — unreadable / absent / unsigned / below-tier / stands — with unreadable kept on exit 4 and the rest on exit 3. Recognition is imported: heading marker, head-sha match, newest-of and the sha floor from check-half-states.mjs; readServedTier, servedTierStands and the Reviewed-by: key line from check-clause2-carriers.mjs. No second parser; a failed or half load is a refusal.
  4. The lazy import is load-bearing and pinned: the module-scope cycle through check-half-states.mjs is real (measured both ways, exit 13), so the recognisers load from a function body after evaluation, legal only because the dispatch dropped its top-level await (main().then); the battery reads this file's own source for that precondition and ablation D reds exactly that case.
  5. Boundary, accepted as claimed: the leg measures EXISTENCE and PROVENANCE of the review of record (heading, current head, Reviewed-by:, standing Served-tier:), not its verdict word; the PASS reading stays precondition ① of the landing check and is the seat's. The claim (5653641998) specified exactly this shape.
  6. Deviation, declared, accepted: constexport const REVIEWED_BY_LINE in check-clause2-carriers.mjs, value expression unchanged (md5 equal before and after) — one keyword outside the claimed surface, forced by rule 3 above.
  7. Coupling noted: PR feat(pm): two report-only patrol rows — the decision card's four-facet block, and finding outliving its grade #18033 (in flight) edits check-half-states.mjs but keeps the four names this leg imports; whichever lands second runs the merge-group checks on the merged tree and the battery's live loadRecordRecognisers() exercises the real edge.

Seat measurements on the head tree (git archive f088df57): check-governed-queue-guard.mjs --self-test → 229 cases pass, exit 0 (base 183; one battery added, floor 19 → 20). --pair 18036 → exit 0 at 2026-09-13T14:47Z (cards #18020 and #17950 both agree). Checks on f088df57 at 2026-09-13T14:32Z: 35 runs, 0 red, 0 running. The dev's four ablations (prefix slash, any-record-state, top-level await in each dispatch) each red the named cases and restored by blob hash.

② semver: scripts/pm/** publishes nothing; skip-changeset is right.

③ boundary flags: open_questions empty. Two 「noted, not filed」 items stand as notes (the stale reason in makeLabelReader's docblock; the objectui guard copy is this seat's own card, filed at landing). Landing consequence: a references/-only governed PR with this seat's review of record on its head enqueues without a maintainer click — the enforcement half of #17950.

Implemented-by: claude/issue-18020-queue-guard-references-tier
Reviewed-by: session_01DAcomhvR9kKizeYgg89Vo8

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

@os-project-manager
os-project-manager marked this pull request as ready for review September 13, 2026 14:59
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit 60b9955 Sep 13, 2026
51 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-18020-queue-guard-references-tier branch September 13, 2026 15:24
This was referenced Sep 13, 2026
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pm-dispatch: the merge-queue guard still demands an approval for a references/-only governed PR — #17950's tier is declared, not enforced

2 participants