feat(pm): a PR over 5,000 changed lines lands only by a human merge — size predicate in check-governed-merges --test, the same reading in dispatch-gates, one rule line in SKILL.md and landing-operations - #19033
Conversation
…itions + deletions over 5000 lands only by a human merge Maintainer ruling 2026-09-18, verbatim: 「还有应该完善skills,修改代码量超过某个行数(比如5000)就应该人工审核。」 The threshold is declared once as HUMAN_MERGE_LINE_THRESHOLD = 5000. --pr reads the pair off the same GET that gives changed_files (its absence is a refusal, never a size of zero); --branch counts the merge-base range with --numstat (binary files at zero, as on GitHub); --test takes --additions/--deletions as a pair or says NOT MEASURED on stdout. Either limb exits on the GOVERNED code, so every caller that routes 3 to the human terminal routes an oversized PR there. Generated files are included — a certified regeneration lifts the path off the register and lifts nothing from the size. Self-test: a new battery pinned on both sides of the threshold and on the PR that prompted the ruling. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
…ier verdict The same 2026-09-18 human-merge line threshold the landing gate enforces, read at dispatch time off the worktree's own diff (numstat off the merge base against the working tree, untracked files counted from disk, binary files at zero) so a seat knows before ACCEPT that the PR needs a human. An explicit path list carries no diff and prints NOT MEASURED, never a silent under. The threshold is imported from check-governed-merges.mjs — one declaration, no second copy. --json carries the reading as changedLines; --commands keeps it on stderr with the rest of the provenance. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
…md line, the landing-operations pre-check row, the ceiling 812 → 813 Maintainer ruling 2026-09-18, verbatim: 「还有应该完善skills,修改代码量超过某个行数(比如5000)就应该人工审核。」 SKILL.md gains one 111-byte rule line beside the four-piece-terminal trigger; landing-operations.md folds the size limb into the pre-check row (now spelled --pr N, which reads paths and size in one call) at 117 bytes, ceiling unchanged. The SKILL.md ceiling rises 812 → 813 under the ratchet's own maintainer exit: measured, the rule could not be paid in place — 0 of 598 adjacent bullet pairs merge under the 120-byte cap (smallest 123 B) and the trigger line stands at 118 B. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
…ze-threshold-human-merge
…lock The threshold import moved this file's inherited-population marker from line 702 to 705; the census row that names it by file and line moves with it — the row is named, never counted, so the move is recorded rather than absorbed. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
维护者速读(终稿)· skills 席 · 2026-09-18T12:49Z这个 PR 做什么:您 12:33Z 的字「修改代码量超过某个行数(比如5000)就应该人工审核」落成机械门禁。落地前置检查 在真 PR 上的读数:PR #18971(+238,310 / −119)—— 旧版本判「不受管,照常入队」;这个版本判「HUMAN MERGE,23.8 万行 > 5000」。今天其他 PR(最大 +692)全部「under」。 它还没盖住的两处,已立卡:合并队列的守卫仍只看路径(#:席位若跳过前置检查直接入队,超大 PR 仍会合进去 —— 更急);AGENTS.md §7 还写着「两类 PR 不能凭绿灯合入」,现在是三类(#)。 验收:复核记录 5730219787(PASS)、ACCEPT 5730220167;门禁自测 328 → 369、派发工具自测 1849 → 1862;派生门禁 42 / 42 绿;CI 在跑,截至 12:47Z 无红。 请您做的一件事:Approve(受管文本),或直接 ready + squash 合并。要问的只有一个字:5000 是不是您要的数 —— 是就照批,不是回个数我改常量。 Generated by Claude Code |
|
Correction (skills seat) · 2026-09-18T12:51Z — the review of record 5730219787 and the 维护者速读 5730220889 above name the two follow-up cards with EMPTY numbers (a variable that did not expand when the seat's filing step failed before the post). The cards, filed at 2026-09-18T12:50Z: #19036 (the merge-queue guard's Generated by Claude Code |
One conflicted file, scripts/pm/check-governed-merges.mjs: the tiered register from main meets the SIZE predicate on this branch. Both sides kept: GOVERNED_TIER_* beside HUMAN_MERGE_LINE_THRESHOLD; `tier` beside `humanMerge` on the post-lift verdict; the Tier H block prints its landing-tier line and then the size line; the Tier S block gains the same size line; the self-test summary carries both suffixes; the roster floor is recounted to the 28 batteries the merged file declares (27 and 26 on the two sides, none lowered). The size line's "same terminal" now names Tier H, the terminal the rules layer keeps under the tiers. SKILL.md, the ratchet ceiling and landing-operations.md merged clean: 813 lines, ceiling 813. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
…lists one — the SIZE limb reaches the queue The 2026-09-18 ruling landed seat-side (PR #19033) while the queue leg of check-governed-queue-guard.mjs kept handing testVerdict no size, so a PR over HUMAN_MERGE_LINE_THRESHOLD changed lines that a seat enqueued still merged. Queue guard: a fourth leg reads every queued pull request's additions / deletions off the same pull object the head read uses (makePullReader grew a `size`, no second endpoint), judges it through the sibling's IMPORTED predicate (testVerdict([], { size }) + landsByHumanMerge — this file declares no threshold and spells no comparison, pinned against its own source), and REFUSES on exit 8 with the two numbers, the threshold and the human-merge remedy printed; an unreadable size fails CLOSED on exit 9; the pull_request leg stays silent and byte-identical; exit precedence governed > size > carrier is a pure function, pinned on every combination. Post-merge sweep: classifyCommit is now landsByHumanMerge on testVerdict, the size read LOCALLY off the landed diff by one `git diff-tree --numstat` per mainline commit (commitChanges, replacing commitPaths' --name-only with a byte-identical path list), so an oversized landing with no governed path is an entry on the size limb alone — counted apart in the head, listed with a ⛔ SIZE row and the same attribution column. Self-tests: guard 261 → 296, governed-merges 410 → 435. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5y9kRg1YtYaMQYExVLRc2
…ules match (objectstack-ai#19045) Fixes objectstack-ai#19025 Clause-②: no — one references-layer file, three lines, line-neutral. No package, export, schema or generated artifact moves, and nothing published changes. ## The finding, and what this PR owes A Claude Code `Bash(...)` permission rule is a literal PREFIX match up to its first glob. The landing-call allow rules the maintainer is committing read ```text Bash(curl -sS -X POST https://api.github.com/repos/objectstack-ai/objectstack/pulls/*/ccr/ready_for_review *) Bash(curl -sS -X PUT https://api.github.com/repos/objectstack-ai/objectstack/pulls/*/ccr/auto_merge *) ``` so the operative literal prefix is everything before the first glob — `curl -sS -X POST` (or `PUT`) followed by the bare, unquoted url up to `/pulls/`. A command that puts a `-H` flag before the url, or quotes the url, matches nothing and falls to the session classifier. The two rows named the endpoint but never the invocation, so nothing in the corpus prescribed the flag order. ## Measured on `origin/main` at `dbd474431` before the clause was written - `.claude/settings.json`: **47 allow entries, of which 21 are `Bash(curl ...)` rules**, and all 21 match verb-then-bare-url (`Bash(curl -sS -X VERB https://api.github.com/...` with the globs after). Zero exceptions. The dispatch calls all 47 curl rules; measured, 47 is the *total* allow count and 21 of them are curl rules — the shape claim itself holds for every one of the 21. - The two `ccr/` landing rules are **not yet in** `.claude/settings.json` (`grep -n 'ccr/'` → no hit): they are in flight on the maintainer's side. This PR prescribes the spelling those rules match; it adds no rule, widens none, and says nothing about what the classifier does. - `references/landing-operations.md` is untouched (held by objectstack-ai#19033). Measured there: `grep -n rest-channel` → **zero hits**, and `grep -rn 'ccr/'` over `.claude/` hits only `platform-readings.md:48` and `rest-channel.md`. So that file neither points at `rest-channel.md` nor spells either landing call — the dispatch's mechanism assumption 2 is **falsified**, reported rather than acted on. ## Before / after — three lines, all in `.claude/skills/pm-dispatch/references/rest-channel.md` Ready row (`:48`), 100 B → 120 B: ```text - - ✓ draft 转 ready `POST .../pulls/{n}/ccr/ready_for_review`,反向 `.../ccr/convert_to_draft`。 + - ✓ draft 转 ready `curl -sS -X POST .../pulls/{n}/ccr/ready_for_review -d '{}'`,反向 `.../ccr/convert_to_draft`。 ``` Auto-merge row (`:52`), 109 B → 120 B: ```text - - ✓ auto-merge 挂载 `PUT .../pulls/{n}/ccr/auto_merge` 带 `{"merge_method":"SQUASH"}`,`DELETE` 卸载。 + - ✓ auto-merge 挂载 `curl -sS -X PUT .../pulls/{n}/ccr/auto_merge -d '{"merge_method":"SQUASH"}'`,`DELETE` 卸载。 ``` Section heading (`:34`), 32 B → 120 B — it carries the reason in one clause, and it governs both rows plus the other 19 write rows of the same section: ```text - ## 写侧 —— 全部可迁移 + ## 写侧 —— 全部可迁移;允许规则按首个 glob 前的字面前缀匹配:verb 紧跟裸 url,`-H`/`-d` 后置 ``` No other line changed. Every existing fact of both rows is still on its own row: the `✓`, the draft-to-ready and auto-merge-mount meanings, the reverse `.../ccr/convert_to_draft`, the `{"merge_method":"SQUASH"}` body and the `DELETE` unmount. Both rows stay grep-able by `ccr/ready_for_review` (1 hit) and `ccr/auto_merge` (2 hits), which is how SKILL.md and `platform-readings.md` reach them. ## Line and byte budget (the ceiling is 82 and the file was at 82) - Line count: **82 before, 82 after** — line-neutral, nothing folded, nothing paid, no ceiling touched. - Bytes of every changed line, measured with `LC_ALL=C awk '{print length($0)}'` and cross-checked through the gate's own `classifyLine`: `:34` 120, `:48` 120, `:52` 120. The cap is 120 B, the file's own pre-existing maximum (`:3` is 120), and **no line uses a length exemption**: `scanLineLengths` reports `offenders: []` with `exempt entries: 0`. - Ratchet verdict, verbatim: `✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/references/rest-channel.md is 82 lines (ceiling 82; headroom 0).` ### The dispatch's mechanism assumption 1, measured and falsified A single ≤120 B line **cannot** carry the verb, the bare url, `-H "Content-Type: application/json"`, `-d '{}'` and a reason clause: the command alone is 95 B with the file's `...` url abbreviation and 128 B with the url written out, before any prose or the row's existing facts. Measured packings: the ready row with `-H` included and its reverse-endpoint clause dropped is 119 B — it fits only by shedding an existing fact, and the auto-merge row with `-H` and its `SQUASH` body is 137 B, which the gate classifies `over` (it re-wraps to 2 lines). The fallback in the dispatch (reason on the ready row, a pointer on the auto-merge row) does not fit either — the rows are at 120 B with their own facts. So the invocation shape stayed on the rows and the reason moved up one level, to the section heading that governs them, at 120 B. `-H "Content-Type: application/json"` is not repeated in the two commands because the row four lines above the first one already carries it as a rule for every write (`:44`), and this file's own discipline is ⛔ 不在两处各存一份; the heading names `-H` so the reader knows where it goes. ## Reader test A seat about to land a PR greps `ccr/ready_for_review`, lands on `:48`, and types `curl -sS -X POST https://api.github.com/repos/objectstack-ai/objectstack/pulls/19033/ccr/ready_for_review -H "Content-Type: application/json" -d '{}'` — verb first, bare url next (the `...` in the row is this file's abbreviation for `https://api.github.com/repos/{o}/{r}`, established at `:7` and used by 21 of the file’s rows), header after. That command's first bytes are the allow rule's literal prefix, so it never reaches the classifier. Reading the heading tells the seat *why* the order is not a style choice. ## Gates Derived from this worktree with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (the tool takes its own change set from the merge base — 1 path). 17 families derived, **17 run, all exit 0**, exit codes captured redirect-then-`$?`; reconciliation: `✓ dispatch-gates --ran: 17 derived famil(ies) accounted for — 17 run, 0 NOT-MEASURED`. Named in the dispatch and green here: `check:pm-skill-ratchet`, `check:pm-skill-id-lint`, `check:nul-bytes`, `check:skill-frame-sync`, `check:doc-authoring`, `check:pm-governed-merges`. `check:doc-formula-expressions` answered `exit 3` (PREREQUISITE NOT MET — nothing measured) until `@objectstack/formula` and `@objectstack/lint` were built under `scripts/pm/os-verify-lock.sh`; it is `exit 0` after the build. `check:pm-settings-deny-roster` was run beyond the derivation because its roster lives under `.claude/`, the directory this diff is in — `exit 0`, 17 declared content-write tools = 17 enforced deny entries. `check:pm-dispatch-gates` exceeds the foreground cap and was detached; its verdict is reported in the dev report rather than guessed here. Repo-wide `pnpm lint` and the rest of the farm are CI's run, not this PR's local scope. ## 维护者速读(草稿) **改了什么** —— PM 技能的 `references/rest-channel.md` 里,「转 ready」和「挂 auto-merge」两行原来只 写了 endpoint,现在直接写出席位该敲的那条 `curl` 命令;该节的标题多了一句话,说明为什么命令必须以 「动词 + 裸 url」开头(允许规则按首个 glob 前的字面前缀匹配),`-H`/`-d` 一律后置。 **为什么改** —— 维护者正在提交的四条落地允许规则是字面前缀匹配。席位按习惯写法(先 `-H`、url 加引号) 敲出的命令一条规则都不匹配,会落到会话分类器,于是「七个绿 PR 等着人来点」的症状在规则齐备后照样复现。 规则文本不动,本 PR 只补事实层的拼写。 **风险与代价(含回滚)** —— 风险极低:改的是三行说明文字,不碰任何代码、生成物或发布内容;两行仍可被 `ccr/ready_for_review`、`ccr/auto_merge` grep 到(SKILL.md 与 `platform-readings.md` 靠这两个 token 指过来)。代价是两行与标题都顶到 120 字节上限,下次再往这三行加字就得先折行或搬走一个事实。回滚 = revert 这一个提交,无迁移、无后续动作。 **席位意见** —— **你要做的** —— 无需动作;这是事实层(`references/`),按席内契约档复核后进队列。若你更希望「为什么」 那句话落在两行自己身上而不是节标题上,请说一声:那需要把 82 行的天花板抬到 83,而抬天花板要你的裁决。 ## Out of scope, noted, not filed - The dispatch's mechanism assumption 2 is falsified (`landing-operations.md` points nowhere and spells no `ccr/` call). Not filed as a card: the file is held by open PR objectstack-ai#19033, whose author is the next one to touch those rows. - `.claude/settings.json` carries no `Bash(curl -sS -X POST https://api.github.com/repos/objectstack-ai/objectstack/pulls *)` rule, so opening this PR through the REST proxy still reaches the classifier. Observation only — the allow-rule text is the maintainer's. --- _Generated by [Claude Code](https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF)_ Co-authored-by: Claude <noreply@anthropic.com>
…n alone — over 5,000 changed lines (objectstack-ai#19042) Fixes objectstack-ai#19037 Clause-②: no ## What changed `AGENTS.md` Multi-agent discipline §7 said 「Two classes of PR never enter this path on green alone」 — (a) a governed-surface diff, (b) the Version Packages / release-performing PR. The maintainer's ruling adds a third, and the rules layer contradicted the landing rule until it said so. The paragraph now names three classes: > (c) a PR whose **changed lines exceed 5,000** (`additions + deletions`, generated files included) — it lands only by a human merge, which is its review record. and the closing instruction reads the size in the same breath as the file list and the author: 「Read the PR's file list (`get_files`), **its author and its size** before you arm anything.」 Ruling (verbatim, untranslated), recorded on objectstack-ai#16045 (comment 5729462393) and carried by objectstack-ai#19012 / PR objectstack-ai#19033: > 「还有应该完善skills,修改代码量超过某个行数(比如5000)就应该人工审核。」 Read as the landing rule already spells it: `additions + deletions` on the PR, strictly greater than 5,000, generated files INCLUDED, no exemption; the terminal is the human's merge. The sentence states the ruling, not a line number of PR objectstack-ai#19033, so it reads true whether or not that PR has landed. No new rule beyond the ruling: no exemption, no lower number, no review that is not the human's merge. **Why the sentence carries no card number or date.** `AGENTS.md`'s header: a rule 「carries no incident narrative, no ruling date or quotation, and no issue-number citation (`pnpm check:pm-skill-id-lint`) — a rule's provenance lives in the PR that landed it」; that gate's pattern is `/#[0-9]{3,}/` over `AGENTS.md`, so `objectstack-ai#16045` / `objectstack-ai#19012` in the sentence would go red. The dispatch's mechanism assumption 1 (name the ruling by card) is falsified by the gate and by the file's own header — the provenance is this PR body, and the sentence is self-contained. ## Line budget — net 0 at the ceiling (1099 / 1099) The paragraph was wrapped at ~88 bytes with a 27-byte last line; re-flowed at the file's own 120-byte per-line budget (the Prime Directives block runs to 120 bytes; §8 beside it to 116) it absorbs the clause with no new line. Every bold and code span stays whole on its line, as before. | line | before (bytes) | after (bytes) | |---|---|---| | 495 | 90 | 113 | | 496 | 88 | 117 | | 497 | 88 | 109 | | 498 | 81 | 109 | | 499 | 86 | 111 | | 500 | 27 | 75 | | paragraph | 466 | 640 | | file | 1099 lines | 1099 lines | Before: ```text ⛔ **Two classes of PR never enter this path on green alone:** (a) a diff touching any **governed surface** (**Prime Directive objectstack-ai#14**, which names them and holds the current list — **this file and `CLAUDE.md` are on it**, so re-read it rather than recalling it); (b) the **Version Packages** PR, or any PR whose merge performs a release (**Prime Directive objectstack-ai#15**). Read the PR's file list (`get_files`) **and its author** before you arm anything. ``` After: ```text ⛔ **Three classes of PR never enter this path on green alone:** (a) a diff touching any **governed surface** (**Prime Directive objectstack-ai#14**, which names them and holds the current list — **this file and `CLAUDE.md` are on it**, so re-read it rather than recalling it); (b) the **Version Packages** PR, or any PR whose merge performs a release (**Prime Directive objectstack-ai#15**); (c) a PR whose **changed lines exceed 5,000** (`additions + deletions`, generated files included) — it lands only by a human merge, which is its review record. Read the PR's file list (`get_files`), **its author and its size** before you arm anything. ``` ## Reader test A seat about to arm a 6,000-line PR reads §7 and stops: 「Three classes … (c) a PR whose changed lines exceed 5,000 … lands only by a human merge」. Before this PR the same seat read two classes, found a 6,000-line diff in neither, and armed. ## Other mentions in `AGENTS.md` (mechanism assumption 3) `grep -n -i -E 'green alone|two classes|Version Packages'` on `origin/main` `dbd474431`: - :291, :299, :307, :309 — Prime Directive objectstack-ai#15's own text (no seat merges the Version Packages PR; that merge is the release trigger). Left: it is class (b)'s anchor, not an enumeration of the never-on-green-alone set. - :495, :498 — the §7 sentence. Changed (this PR). - No other line enumerates the set. Two neighbours name class (a) alone by design and were left: the Skills section's 「Both roots are governed surfaces — human-merge only, or Prime Directive objectstack-ai#14's pinned-approval path」 (about governed surfaces only), and Post-Task Checklist step 2's 「⛔ Except a diff touching a governed surface」, which defers to §7 by reference for the arming rule — see Acceptance notes. ## Gates (local, at `845470659`) `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 14 families from the changeset (1 path vs merge base `dbd474431`); all 14 ran, exit codes captured redirect-then-`$?`, plus the two the dispatch named: ```text node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 pnpm check:agent-test-spelling :: exit 0 pnpm check:docs-audit-scope :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:pm-governed-merges :: exit 0 pnpm check:pm-governed-prose :: exit 0 pnpm check:pm-skill-id-lint :: exit 0 pnpm check:pm-skill-ratchet :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:required-contexts :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:skill-frame-sync :: exit 0 pnpm check:doc-authoring :: exit 0 ``` Reconciliation: `✓ dispatch-gates --ran: 14 derived famil(ies) accounted for — 14 run, 0 NOT-MEASURED`. Ratchet: `✓ check-skill-line-ratchet: AGENTS.md is 1099 lines (ceiling 1099; headroom 0).` and `widest table row is 768 bytes (pin 768; headroom 0)`. Id-lint: 0 citations in `AGENTS.md`. Control-byte scan of the file: no match. Not measured locally: `check:doc-formula-expressions` (a CEL gate over fenced formula examples in the docs/skills corpus; this diff adds no fenced block and the family is outside the derivation; CI runs it). `check:pm-dispatch-gates` ran detached — its verdict is in the report comment on objectstack-ai#19037. ## Changeset `skip-changeset`: `AGENTS.md` is a repo-root instruction file; the root package is private and no package `files[]` ships it — nothing published moves. ## Acceptance notes - noted, not filed: Post-Task Checklist step 2 names only the governed class in its 「⛔ Except」 clause; the size class reaches a dev's own PR too. It defers to §7 by reference, and the file header's one-statement-per-rule principle argues against restating; a fold there is one line of re-flow if the seat wants it. 承接者: skills seat. - noted, not filed: the size class has no Prime Directive of its own (it is a ruling, not a directive); a numbered home would be a new directive beside objectstack-ai#14 / objectstack-ai#15 — a separate decision, not taken here. ## 维护者速读(草稿) - **改了什么**:`AGENTS.md` 多 agent 纪律 §7 那句「两类 PR 绿了也不能自动进合并队列」改成三类:新增 (c) 改动行数(additions + deletions,含生成文件)超过 5,000 行的 PR,只能由人工合并。文件行数不变(1099 / 1099),只是把那一段按文件自身的行宽重排。 - **为什么改**:您 09-18 的裁决「修改代码量超过某个行数(比如5000)就应该人工审核」已经落进了 PM 的落地规则与门禁脚本,但规则层 `AGENTS.md` 还写着两类,座席读到的规则与落地规则互相矛盾。 - **风险与代价(含回滚)**:纯文本改动,不动任何脚本或门禁;本地 16 项门禁全绿。回滚 = revert 这一个 commit。 - **席位意见**:(留空,由席位定稿) - **你要做的**:确认这句话与您的裁决一致(尤其「含生成文件、无豁免」与「人工合并即审核记录」两点),然后由您合并。 --- _Generated by [Claude Code](https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF)_ Co-authored-by: os-dev <elon@objectstack.ai> Co-authored-by: Claude <noreply@anthropic.com>
…erge sweep lists one — the SIZE limb reaches the queue (objectstack-ai#19175) Fixes objectstack-ai#19036 Clause-②: no ## What this PR does The maintainer's 2026-09-18 ruling — 「修改代码量超过某个行数(比如5000)就应该人工审核」 — landed seat-side in PR objectstack-ai#19033 (`HUMAN_MERGE_LINE_THRESHOLD = 5000`, `testVerdict(paths, { size })`, `sizeVerdict`, `landsByHumanMerge` in `scripts/pm/check-governed-merges.mjs`). The merge-queue guard's `merge_group` leg kept handing the predicate no size, so a PR over 5,000 changed lines that a seat enqueued still merged — exactly what PR objectstack-ai#18971 did (+238,310 / −119, nothing governed). This PR carries the limb to the two places the card names. **Queue guard (`scripts/pm/check-governed-queue-guard.mjs`)** — a fourth leg, `runSizeGuard` / `sizeGuardVerdict` / `renderSizeVerdict`: - reads every queued pull request's `additions` / `deletions` off the same pull object the head read uses — `makePullReader` grew a `size` field (through the sibling's own `pullSizeFrom`), no second endpoint, no new workflow scope (`pull-requests: read` already covers the pull object; the workflow file is untouched and the self-test pins that no `issues:` scope appeared); - judges it through the sibling's IMPORTED predicate: `testVerdict([], { size })` is the size limb alone (the path limb is the governed leg's question, already answered on the lifted rows), and `landsByHumanMerge` reads it — this file declares no threshold and spells no comparison, pinned against its own source; - REFUSES on a new exit code `EXIT_REFUSED_OVERSIZED = 8`, printing the two numbers, their sum, the threshold, the limb (SIZE), the source (`GET /repos/{o}/{r}/pulls/{n}`) and the one remedy — a human merge (the ruling quoted untranslated; it never advises making the diff smaller); - an unreadable size (the pull read throws, or the object carries no pair) is `EXIT_REFUSED_SIZE_UNREADABLE = 9`, fail-CLOSED — this read DECIDES, unlike the PR head, which has decided nothing since 2026-09-04; - the `pull_request` leg is silent and read-free (renders `''`), so that leg's output stays byte-identical; - the three-leg exit precedence is a pure function, `groupExitCode`: governed, then size, then carrier — every block is always printed, one code exits. **Post-merge sweep (`check-governed-merges.mjs` default mode)**: - `classifyCommit` is now `landsByHumanMerge(testVerdict(paths, { size }))`, so a landing is an entry when EITHER limb fired — a governed merge as before, and an oversized landing with no governed path at all; - the size is read LOCALLY off the landed diff: `commitChanges` runs one `git diff-tree --numstat --no-renames -m --first-parent` per mainline commit (replacing `commitPaths`' `--name-only`; the self-test pins the path list byte-identical against `--name-only` on a real fixture, a merge commit read against its first parent, a binary row at zero); - `renderReport` counts such rows apart (`N governed merge(s) and M oversized landing(s) with no governed path`), prints a `⛔ SIZE:` row with the numbers and the threshold, keeps the same attribution column, and prints GitHub's own pair beside the landed number only when the two differ (it rides the attribution GET the row already pays for; it never decides the listing); - `--json` entries carry `size` and `humanMerge`. ## The PM's mechanism assumptions, measured - 「the sweep already reads each merged PR (it prints `merged_by`)」 — **falsified.** The attribution loop reads `GET /pulls/{n}` only for rows `classifyCommit` already produced (governed merges); an ungoverned landing is never read at all, so no API read could have made it an entry. Route taken instead: the local `--numstat` reading above, the same source `--branch` already uses in this file, zero API, and it also lets the sweep classify before any attribution is spent. - 「the size reading cannot ride the existing head read for an oversized PR with no governed path; one `GET /pulls/N` per PR in the group」 — **held.** The size leg reads every queued PR through the same reader; the call count is pinned (`apiCalls === carrierPullsInGroup(rows).length`, deduplicated, group order). - 「fail-closed on an unreadable size」 — **taken**, on a code of its own (9) rather than the governed leg's 4, for the same reason 6/7 are split from 3/4: the legs' refusals must stay separable in a log. - 「pick the exit code already highest in the table」 — **taken**: a governed-unsatisfied AND oversized group prints both limbs and exits 3. Consequence worth stating: a merge group naming no pull request now exits on the size leg's 9 rather than the carrier's 7 (both blocks still print their own refusal); pinned. - **Boundary not decided here:** no authorized APPROVED review and no review of record lifts the size limb — the landed predicate says a human MERGE, and nothing has ruled the number the way 2026-08-27 ruled paths. The rendering says so; widening it is a one-line maintainer decision in the sibling. Listed under open questions in the report, not implemented. ## Acceptance (the seat's checklist) - oversized PR, NO governed path, in a merge group → refused, size limb named: the objectstack-ai#18971 replay pin (`⭐ objectstack-ai#18971-replay-an-OVERSIZED-PR-with-NO-governed-path-is-REFUSED-at-the-queue-on-the-size-code`) — governed leg clear at zero reads, size leg exit 8, `groupExitCode` 8; the rendered text names `objectstack-ai#18971`, `238429 changed line(s) (+238310 / -119)`, `EXCEEDS the human-merge line 5000`, `HUMAN MERGE`; - governed path AND oversized → both limbs rendered, one exit (3), pinned; - exactly at the threshold → clear (guard: `⭐ exactly-the-threshold-is-WITHIN-the-comparison-is-strictly-greater`; sweep: `⭐ exactly-the-threshold-is-NOT-listed-the-comparison-is-strictly-greater`, and a real fixture commit of exactly 5,000 lines is not listed); - `pull_request` leg byte-identical: `renderSizeVerdict` returns `''` there and a throwing spy proves zero reads; - unreadable size → exit 9 (throw, missing pair, no recorded reading, group naming no PR), never a pass; - sweep lists an oversized merged PR, not an at-threshold one: unit pins plus a REAL CLI sweep over a fixture repo (`PR objectstack-ai#5001` listed with `⛔ SIZE`, `PR objectstack-ai#5000` not), on stdout and in `--json`; - self-tests green with counts up: guard 261 → 296, governed-merges 410 → 435; battery rosters +1 each, floors 21 → 22 and 29 → 30; - workflow file untouched. ## Verification Self-tests (worktree at `5c7caff` — the branch merged with `origin/main` `c229223`, which touched neither file; after `pnpm install`): - `node scripts/pm/check-governed-queue-guard.mjs --self-test` → exit 0, `296 cases pass` (was `261 cases pass` at `e8667ee`). - `node scripts/pm/check-governed-merges.mjs --self-test` → exit 0, `435 assertions` (was `410 assertions`). Ablation — the comparison inverted in the SIBLING, watched from both files (proves the guard imports the predicate rather than restating it), through `scripts/ablation-replace.mjs` in WRAP mode against the committed head `5c7caff` (the final of three runs; the first two are recorded below because each taught something): - mutation: anchor `exceeds: changedLines > HUMAN_MERGE_LINE_THRESHOLD,` × 1 → the same line with the greater-than sign replaced by a less-than sign, × 1; blob `6e1112bcd55e` → `c7a42160aabd`; on-disk counts read back inside the mutated window: replacement 1, original 0. - guard self-test under mutation: exit 1, `14 of 296 case(s) failed`, all in the objectstack-ai#19036 battery (`threshold-plus-one-changed-line-is-OVERSIZED`, `one-under-is-WITHIN`, `an-OVERSIZED-queued-PR-REFUSES-with-code-8`, `a-within-sibling-does-NOT-carry-an-oversized-PR-through-the-group`, `objectstack-ai#18971-replay-an-OVERSIZED-PR-with-NO-governed-path-is-REFUSED-…`, `governed-AND-oversized-prints-BOTH-limbs-…`, `an-authorized-APPROVAL-…-lifts-NOTHING-from-the-size-…`, `a-certified-pure-regeneration-lifts-…-NOTHING-from-the-size-at-the-queue-either`, …). - governed-merges self-test under mutation: exit 1, `30 failure(s)`, 0 TypeErrors — the pre-existing SIZE battery (`5001-changed-lines-is-OVER-it`, `the-PR-that-prompted-the-ruling-reads-238429-…`, `--test-with-5001-changed-lines-…-exits-3`, …), the new objectstack-ai#19036 battery (`an-oversized-landing-with-NO-governed-path-is-a-sweep-ENTRY-…`, `the-head-counts-the-oversized-landing-APART-…`, `a-REAL-sweep-LISTS-the-over-by-one-landing-…`, …) and two objectstack-ai#13307 live-mirror sweep pins that now see a phantom oversized row (every small landing reads over an inverted line — the expected direction). - restore: blob after restore `6e1112bcd55e…` == `HEAD` blob, `git diff HEAD` empty, `git status --porcelain` empty, anchor count back to 1. - Run 1 (on `3c7f5ec`): guard 14/296 red as above; the merges leg exited 1 by a **TypeError** in my new battery (a pin dereferenced the fixture that classifies to null under the mutation), so its failures were not named — fixed in `28faa51` (null-guarded pins; a red case must be a named one). Run 2: run against that fix while it was still UNCOMMITTED; the tool restores to `HEAD` by design, so the fix was discarded by the restore — caught by the pre/post blob compare (`6e1112…` before, `859965…` after), re-applied, committed, and run 3 is the record above. The "commit the fix first" rule, measured on the fix to the pins. Gates (derived in the worktree with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` — 33 commands, identical to the dispatch's list; reconciled with `--ran`; exit codes captured after redirection, never through a pipe; run at `5c7caff`, the final head, after the ablation's restore was proven): | # | command | exit | verdict line (from the gate's own output) | wall | |---|---|---|---|---| | 1 | `node scripts/check-ci-filter-parity.mjs` | 0 | OK: all 184 declared cross-package glob(s) (131 unique) are covered by `core` or `crosspkg`, every `crosspkg` entry still covers one, and the `test` job's `if:` | 0s | | 2 | `node scripts/check-closing-keyword-parity.mjs` | 0 | check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators; sweep found 5 file(s) carrying the grammar across 8993 tracked | 1s | | 3 | `node scripts/check-closing-keyword-parity.mjs --self-test` | 0 | ✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red. | 3s | | 4 | `node scripts/check-comment-mask-corpus.mjs` | 0 | ✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 6896 files, 0 disagree, 0 unparseable, 81.9s (comparator self-test: 26 cases pass). | 83s | | 5 | `node scripts/check-declaration-mirrors.mjs` | 0 | OK: 10 hand-written declaration(s) agree with their modules on name, kind and required arity. | 0s | | 6 | `node scripts/check-declaration-mirrors.mjs --self-test` | 0 | All 29 self-test cases passed. | 0s | | 7 | `node scripts/check-scripts-symbol-anchors.mjs` | 0 | ✅ check-scripts-symbol-anchors: 3524 anchors across 265 scripts resolve — 52 symbol (52 declaration, 0 literal), 3472 file-level, 0 cross-repo, 1 exempt, 2 cont | 4s | | 8 | `node scripts/check-scripts-symbol-anchors.mjs --self-test` | 0 | ✅ check-scripts-symbol-anchors --self-test: every finding class provoked, comment-prose projection wired, declined shapes counted not missed, allowance rows exa | 4s | | 9 | `node scripts/check-self-test-wired.mjs` | 0 | ✓ check-self-test-wired: every one of the 214 script(s) CI runs that ship a `--self-test` has that self-test run by CI. | 2s | | 10 | `node scripts/check-self-test-wired.mjs --self-test` | 0 | check-self-test-wired --self-test: 3 live ledger row(s) verified, plus the comment mask, the right boundary, alias resolution and both audit directions — 10 dec | 2s | | 11 | `node scripts/check-self-test-workflow-commands.mjs` | 0 | ✓ check-self-test-workflow-commands: no self-test CI runs prints a line the Actions runner would parse as a workflow command. | 51s | | 12 | `node scripts/check-self-test-workflow-commands.mjs --self-test` | 0 | check-self-test-workflow-commands --self-test: both measured parse rules pinned (legacy form anywhere in a line, current form only at line start), the innocent- | 3s | | 13 | `node scripts/check-skills-token-ratchet.mjs` | 0 | ✓ check-skills-token-ratchet: 34 authored bundle file(s) within their ceilings; 10 generator-owned file(s) measured, not ratcheted. | 0s | | 14 | `node scripts/check-skills-token-ratchet.mjs --self-test` | 0 | ✓ check-skills-token-ratchet self-test: 65 cases pass. | 1s | | 15 | `node scripts/check-whole-set-label-write.mjs` | 0 | ✓ check-whole-set-label-write: 0 violations — 328 file(s) over 3 root(s) · 12 raw mention(s) · 12 in comments/prose (cleared) · 0 in EXECUTABLE content (judged) · 191 `uses:` pin(s) over 18 di | 2s | | 16 | `node scripts/check-whole-set-label-write.mjs --self-test` | 0 | ✓ check-whole-set-label-write --self-test: all cases pass (24 fixture trees + 5 refusals + 1 allowlist hatch) | 0s | | 17 | `node scripts/pm/bare-root-worklist.mjs --self-test` | 0 | OK self-test: 81 live row(s), 59 unreachable as spelled, 46 recorded verdict(s) — none stale, none missing, none contradicted (12 row(s) whose gate carries the | 29s | | 18 | `node scripts/pm/check-governed-queue-guard.mjs --self-test` | 0 | ✓ check-governed-queue-guard self-test: 296 cases pass (register-driven verdicts, the queue/PR event split, latest-decisive approval reduction, the 2026-09-04 a | 0s | | 19 | `pnpm check:agent-test-spelling` | 0 | ✓ check-agent-test-spelling: 0 violations — 518 file(s) · 9204 bare `--` token(s) · 1749 launcher-rooted run(s) · 13 separator(s) JUDGED · 6 vitest-backed scrip | 4s | | 20 | `pnpm check:bash32-floor` | 0 | ✓ check-bash32-floor: 31 tracked shell file(s) under scripts/**, .claude/hooks/**, .githooks/** name no bash 4+ construct outside a comment, a guarded ${VAR:-} | 2s | | 21 | `pnpm check:cli-command-ids` | 0 | ✓ check-cli-command-ids: 63 module(s) under packages/cli/src/commands examined, all of them default-export a class whose inheritance chain reaches oclif's `Comm | 11s | | 22 | `pnpm check:closing-target-claim` | 0 | ✓ check-closing-target-claim self-test: 105 cases pass. | 1s | | 23 | `pnpm check:cross-package-test-inputs` | 0 | OK: 29 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split "test:repo" task); 13 walked root(s) ju | 20s | | 24 | `pnpm check:driver-memory-census` | 0 | check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states "objectstack-ai#6664 census: 2 ruled consumers". This | 4s | | 25 | `pnpm check:entry-guard` | 0 | ✓ check:entry-guard: 265 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 206 export bindings, 206 of them inert on import (0 known-unsafe, ⛔ S | 47s | | 26 | `pnpm check:nul-bytes` | 0 | check-nul-bytes: OK (scanned 8986 text file(s) -- 8986 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes). | 3s | | 27 | `pnpm check:parse-guard` | 0 | ✓ check:parse-guard: 264 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs. | 2s | | 28 | `pnpm check:pm-governed-merges` | 0 | ✓ check-governed-merges --self-test: 435 assertions (the unified governed predicate + near misses, subject→PR spellings, window parsing, the objectstack-ai#12633 landing wind | 6s | | 29 | `pnpm check:pnpm-filter-targets` | 0 | ✓ check:pnpm-filter-targets: 151/199 `--filter` occurrence(s) across 40 file(s) resolve against 81 workspace package(s); 48 not judged (2 foreign, 26 interpolat | 3s | | 30 | `pnpm check:ratchet-remedy-authority` | 0 | OK check-ratchet-remedy-authority: 259 scripts swept (scripts/*.{mjs,mts} + scripts/pm/*.{mjs,mts}); 15 mark the expanding remedy ⛔ MAINTAINER-ONLY, 5 turn it | 4s | | 31 | `pnpm check:refd-timer-probe` | 0 | OK check-refd-timer-probe: 6891 source file(s) swept; the process-global timer probe is read in packages/qa/refd-timer-testkit/src/index.ts and nowhere else. | 12s | | 32 | `pnpm check:watch-hint-literal` | 0 | ✓ check-watch-hint-literal: 71 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 47, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECLARED_WATCH | 5s | 32 command(s); 0 non-zero exit(s). Reconciliation (`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran` over a record whose every line carries `:: exit N`): `33 derived, 32 run, 0 NOT-MEASURED, 1 UNRUN` — the one unrun family is `pnpm check:pm-dispatch-gates`, which is **NOT MEASURED at PR-open time**: it runs only under the shared verify lock (≈1,000 s under contention); a first run started on `3c7f5ec` overlapped the ablation window (`dispatch-gates.mjs` imports `sizeVerdict` from the sibling, so a child it spawned in that window could have read the inverted comparison), and a second run on `5c7caff` is queued behind it. Its verdict lands in the `os-dev-report` comment on objectstack-ai#19036, not here — this body is written once. Lint, narrowed and measured: `npx eslint --no-inline-config --format json` on the two changed files → exit 0, 2 files, 0 errors, 0 warnings. Narrowing evidence: ① eslint's configured population (`eslint.config.mjs`, the `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` block) includes `scripts/pm/*.mjs`, and `--print-config` resolves a config for the file (parser `typescript-eslint/parser`); ② the JSON output counts 2 files; ③ type-aware linting is not enabled (`parserOptions.project` and `projectService` both absent in the resolved config), so this diff cannot move any untouched file's verdict. The repo-level `pnpm lint` sweep is CI's run. ## Acceptance notes (noted, not filed) - The `pull_request` event payload carries `additions` / `deletions`, so a size early warning on the PR leg would cost zero reads. Not taken: the ruling is about the landing, the seat-side pre-check already refuses before arming, and the PR leg's byte-identity is a standing constraint. 承接者: the skills seat, if the maintainer wants the forecast. - The carrier leg and the size leg each read `GET /pulls/{n}` once per queued PR — the same endpoint twice. A shared per-run pull read would halve it; kept separate so each leg's refusal and count stay separable in a log. Groups are small. 承接者: none. - No governed reference text enumerates the guard's exit table, so codes 8/9 leave no Tier S doc stale (`grep` over `.claude/skills/pm-dispatch/references/**` and `SKILL.md` for `exit 6`/`exit 7`/`EXIT_REFUSED_CARRIER`: 0 hits). `skip-changeset`: `scripts/pm/**` publishes nothing from any released package (no `files[]` of any package ships it). --- _Generated by [Claude Code](https://claude.ai/code/session_01W5y9kRg1YtYaMQYExVLRc2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…aimed region, not the same file — SKILL.md :441 and its core-rules twin (objectstack-ai#19317) Fixes objectstack-ai#18891 Clause-②: no Ruling-ref: 5727164406 — director seat, batch 157 item 2, letter 甲, maintainer 「其他同意」 2026-09-18T08:10Z ## What changed — two lines, one word each, net 0 in both files | File | Line | Before | After | Bytes | |---|---|---|---|---| | `.claude/skills/pm-dispatch/SKILL.md` | :441 | 「- 同文件单跨轮硬串行;延后不是搁置,被延后那一刻就把已知的坑记到该 issue 上。」 | 「- 同区域单跨轮硬串行;延后不是搁置,被延后那一刻就把已知的坑记到该 issue 上。」 | 107 → 107 | | `.claude/skills/pm-dispatch/references/core-rules.md` | :105 | 「- 同批独立性按文件面不相交判,⛔ 不按包;同文件硬串行,冲突交合并队列仲裁 ⛔ 不手排。」 | 「- 同批独立性按文件面不相交判,⛔ 不按包;同区域硬串行,冲突交合并队列仲裁 ⛔ 不手排。」 | 119 → 119 | Line counts: SKILL.md 813 / 813, core-rules.md 151 / 151. `:437` is untouched; `SINGLE_CLAIM_PATHS` stays the only whole-file single-writer set; the 「延后不是搁置…」 half stays byte-verbatim; the core-rules twin lands in the same PR as SKILL.md:44 orders. No script, no gate, no other line of either file. The commit is `5676515` on `claude/issue-18891-region-level-serial`, base `801415a`. ## Why this rendering and not the ruling's sentence verbatim — measured, not assumed The ruling's :441 text 「同区域(认领申报的文件面重叠)单跨轮硬串行;同文件不同区域是普通并发,后落地方解冲突。」 is 123 bytes as a bullet line, and 198 bytes with the 「延后不是搁置…」 half the ruling keeps. `scripts/pm/check-skill-line-ratchet.mjs` (`pnpm check:pm-skill-ratchet`, not `check:pm-governed-prose` as the dispatch assumed) caps every non-exempt line of this file at `MAX_LINE_BYTES = 120` and holds the file at ceiling 813 with headroom 0, so the ruling's own sentence cannot land as written under the file's own gate without a script edit that both the ruling (「⛔ no new script, no gate」) and the dispatch (net 0, no script) refuse. Readings taken on the real gate, each mutation restored to the HEAD blob (hash-verified, `git diff HEAD` empty) before the next: | Run | :441 rendering | `check:pm-skill-ratchet` verdict | |---|---|---| | R0 | unmodified | ✓ SKILL.md is 813 lines (ceiling 813; headroom 0) | | R1 | ruling sentence + deferral half, ONE physical line (198 B) | ✗ 1 line over the 120-byte budget: L441 (198B) | | R2 | ruling sentence (123 B) / deferral half (79 B), TWO lines | ✗ L441 (123B) over budget AND ✗ 814 lines; the ratchet ceiling is 813 | | R3 | ruling sentence with 不同区域→异区域 (120 B) / deferral half, TWO lines | ✗ 814 lines; the ratchet ceiling is 813 | | R4 | core-rules :105 同文件硬串行 → 同区域硬串行 (119 B) | ✓ core-rules.md is 151 lines (ceiling 151; headroom 0) | | landed | :441 同文件 → 同区域 (107 B) plus R4 | ✓ both files at ceiling, headroom 0 (exit 0) | The neighbours cannot absorb the 74-byte deferral half either: :436 to :452 measure 118 / 114 / 112 / 114 / 118 / 107 / 117 / 104 / 67 / 115 / 79 / 111 / 82 / 98 / 120 / 117 / 109 bytes, and the only other line that speaks of deferral (:452, 「阻塞解除后延后单重定价…」, 109 B) has 11 bytes of slack. A bigram-Jaccard scan over the file's 627 bullet lines finds no provable duplicate to retire: the top pairs (J 0.59 down to 0.35) are distinct pointer lines into `references/dispatch-runbook.md` carrying different topics. What the one-word rendering keeps, and what it delegates to lines the ruling left standing: 「同区域…硬串行」 IS the ruling's narrowing (its facet ①: one line narrower, a special case removed). The 「同文件不同区域是普通并发,后落地方解冲突」 clause is exactly what :437 already states for every non-single-writer path (「共享其它路径是普通并发,后落地方解冲突」): once :441 no longer claims the whole file, the same file in different regions is one of those other paths by elimination, and the two lines contradict on no path. The parenthetical definition of 区域 is carried by :449 (「认领申报文件面到区域级,拿不准就串行」, the escape hatch the ruling names as staying), by :278 (「区域写不清就只能整文件串行」) and by `references/seat-post-protocol.md` :20 (every serial-queue entry is 「每张卡认领的区域」). The alternative the seat may prefer at review: land the ruling's sentence as R3 (120 B, two lines) and raise the SKILL.md ceiling 813 → 814 under the ratchet's own maintainer exit (「Raising a ceiling requires a maintainer ruling quoted in the PR」). The precedent is the last landing on this file, e872ef4 (PR objectstack-ai#19033, 2026-09-19): 812 → 813 for one ruled line, with the ratchet comment 「Ruled content is not growth」. That route is a `scripts/pm/check-skill-line-ratchet.mjs` edit and a net +1, both outside this dispatch's constraints, so it is stated here and not taken. ## Tier and landing - `node scripts/pm/check-governed-merges.mjs --test` with both paths: exit 3, GOVERNED — Tier S (席内达档复核落地), `.claude/**` ×2; size NOT MEASURED by `--test` (dispatch-gates' derived count: 4 changed lines, +2 / −2, under 5000). - `node scripts/pm/dispatch-gates.mjs --tier` on both paths: MANDATORY at CONTRACT_REVIEW_TIER (clause ①: the PM dispatch skill MAIN file). - This PR stays DRAFT; it lands on the owning seat's `## Contract review` record for head `5676515` (AGENTS.md Prime Directive objectstack-ai#14). Label: `skip-changeset` (`.claude/**` publishes nothing); no changeset file. ## Gates — derived union on head `5676515`, each exit captured before any pipe `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` from the worktree with no paths (changeset 2 committed / 0 working tree / 0 untracked vs merge base `801415a`) derived 20 commands, identical to the dispatch-time lead; `--ran` reconciles 20 derived / 20 run / 0 UNRUN. Two extras were run because the derivation flags them: `check:pm-settings-deny-roster` (a roster under `.claude`, marked silence-is-not-evidence) and `check:skill-frame-freshness` (pins the four-axis block). | Command | Exit | |---|---| | pnpm check:pm-skill-ratchet | 0 | | pnpm check:pm-skill-id-lint | 0 (27 files clean) | | pnpm check:pm-governed-prose | 0 | | pnpm check:skill-frame-sync | 0 | | pnpm check:skill-frame-freshness | 0 (current with origin/main) | | pnpm check:pm-governed-merges | 0 | | pnpm check:pm-expected-skips | 0 | | pnpm check:pm-half-states | 0 | | pnpm check:doc-authoring | 0 | | pnpm check:nul-bytes | 0 (9034 text files, no raw control bytes) | | pnpm check:agent-test-spelling | 0 | | pnpm check:cross-package-test-inputs | 0 | | pnpm check:driver-memory-census | 0 | | pnpm check:refd-timer-probe | 0 | | pnpm check:watch-hint-literal | 0 | | pnpm check:pm-settings-deny-roster | 0 | | pnpm --filter @objectstack/lint run check:doc-formula-expressions | 3 then 0 — the first run was PREREQUISITE NOT MET (`@objectstack/formula` and `@objectstack/lint` not built; the gate says nothing was measured); after `pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` under the verify lock (4/4 cached), the rerun is exit 0: 58 self-test cases, 22 / 9 / 14 examples judged clean | | node scripts/check-closing-keyword-parity.mjs | 0 | | node scripts/check-closing-keyword-parity.mjs --self-test | 0 | | node scripts/check-comment-mask-corpus.mjs | 0 (6920 files, 0 disagree) | | node scripts/pm/check-governed-queue-guard.mjs --self-test | 0 | | node scripts/pm/check-harness-current.mjs --self-test | 0 | | node scripts/pm/check-governed-merges.mjs --test (both paths) | 3 = GOVERNED, Tier S (the expected code) | The last commit is `5676515` and every command above ran after it. `origin/main` moved by one commit since the base (`e3b3cdd`, metadata-protocol) that touches neither edited file nor the ratchet, so no merge is owed before review. ## Acceptance notes - noted, not filed: SKILL.md :120 (the `priority:p0` table row 「⛔ 不豁免同文件串行、深度等待与认领协议」), its core-rules twin :36, and `references/lanes/services.md` :26 (「同文件卡跨轮硬串行」, a lane-specific stricter rule) still name the serial discipline by FILE. The ruling scoped :441 and :105 only; these are name references to the discipline, not a second definition, so they are a naming nit rather than a defect class. 承接者: the domain:skills seat's next SKILL.md card. - noted, not filed: `check-closing-keyword-parity` prints an informational line that `packages/spec/api-surface-declarations/system.txt` (3,568,357 bytes) exceeds its 2 MiB cutoff for unregistered files; exit 0 and pre-existing on `origin/main`. 承接者: none identified. - The two neighbouring lines the ruling names as staying were read and left alone: :437 (unchanged by the ruling) and :449 (「拿不准就串行」). ## 维护者速读(草稿) ### 改了什么 派发协议「候选与批次」一节里的一条并发规则改了一个词:原来写「同文件单跨轮硬串行」,现在写「同区域单跨轮硬串行」;核心条款摘要里的同一条规则同步改成「同区域硬串行」。两处各改一个词、各不增减行数;`:437`(single-writer 路径只由 `SINGLE_CLAIM_PATHS` 枚举,其它路径普通并发)原样不动。 ### 为什么改 同一节里原有两条互相打架的规则:一条说除 `.objectui-sha` 之外的所有共享文件都是普通并发、后落地的一方解冲突;四行之后另一条说同一文件的卡跨轮必须硬串行。`SINGLE_CLAIM_PATHS` 只有一条,所以对仓里的每一个文件两条规则都给出相反指令;各席位各自选了一条执行,一张停在决策箱里的 PR(25 个 spec 文件)就把 spec 车道大半冻住。裁决(甲)把「硬串行」收窄到「认领申报的文件面重叠的区域」:同文件不同区域是普通并发,后落地方解冲突。改动后,同文件异区域自然落到 `:437` 的「其它路径普通并发」,两条规则在任何路径上都不再冲突。 ### 风险与代价(含回滚) 风险:裁决原句(「同区域(认领申报的文件面重叠)单跨轮硬串行;同文件不同区域是普通并发,后落地方解冲突。」)本身 123 字节,超过本文件 120 字节的行宽门禁;加上裁决要求保留的「延后不是搁置…」半句共 198 字节;文件又顶在 813 行的棘轮上限、余量为零,所以裁决原句无法原样落地(门禁实测:一行写法红,两行写法也红)。本 PR 采用最小改法(一个词),把「同文件异区域普通并发」这半句交给紧邻的 `:437` 承担,把「区域」的定义交给 `:449` 与串行队条款承担。若席位认为裁决原句必须逐字落地,替代路线是按棘轮自己的维护者例外把上限抬到 814(先例:2026-09-19 的 e872ef4),那需要改脚本、净增一行,不在本单授权内。回滚:两个文件各回退一个词,零副作用。 ### 席位意见 (留空,席位定稿) ### 你要做的 无需你操作:这是 Tier S 受管面,由 skills 席位做契约档复核后经队列落地。只有一处可能需要你一句话:若你希望裁决原句逐字进入 SKILL.md(而不是本 PR 的一词改法),请回「抬上限」,席位据此改走 813 → 814 的路线。 --- _Generated by [Claude Code](https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #19012
Clause-②: no
Maintainer ruling (verbatim, 2026-09-18)
Read as: a pull request whose changed line count — GitHub's
additions + deletionson the PR, generated files INCLUDED — exceeds 5,000 lands only by a human merge, at the same terminal as governed text (ACCEPT on the card,needs-user-decisionon the PR, a final 维护者速读, review requested fromGOVERNED_APPROVERS); no seat flips it ready or arms auto-merge. 5,000 is the ruled default (「比如」), declared once asHUMAN_MERGE_LINE_THRESHOLDinscripts/pm/check-governed-merges.mjs, so it moves by one word from the maintainer and one edit. The case that prompted it, PR #18971 (+238,310 / −119, of which 237,706 lines were regenerated artefacts), is the first PR the rule governs — an exemption for generated files would exempt exactly it, so there is none.What changed
scripts/pm/check-governed-merges.mjs— the SIZE predicate.--pr Nreadsadditions/deletionsoff the sameGET /repos/OWNER/REPO/pulls/Nthat giveschanged_files(a PR object missing the pair is a refusal on exit 1 — never a size of zero, never a "not governed" answer);--branch REFcounts the same merge-base range withgit diff --numstat --no-renames(a binary file is 0 lines, as GitHub counts it);--test PATHStakes--additions N --deletions Nas a pair, or printssize: NOT MEASUREDon stdout naming the modes that read it. Either limb exits on the GOVERNED code 3, so every caller that already routes 3 to the human terminal routes an oversized PR there without a new code;--jsoncarriessizeandhumanMerge(governedstays the path limb). A certified generated-artifact regeneration lifts the PATH off the register and lifts nothing from the size. The queue guard'stestVerdict(paths)reading is unchanged (no size handed in ⇒ the path answer as before).scripts/pm/dispatch-gates.mjs— the same reading at dispatch time. With no paths (the derived run) it printsChanged lines — N (+a / -d; generated files INCLUDED) vs the human-merge threshold 5000: underor⛔ OVER — this PR lands only by a HUMAN MERGE …beside the tier verdict (human and--tiermodes), the count on stderr with the rest of the provenance, andchangedLinesin--json. The count is--numstatoff the merge base against the working tree plus untracked files counted from disk (under-derivation refused, like the path list). An explicit path list carries no diff and printsNOT MEASURED, never a silent under. The threshold is imported from the gate — one declaration, no second copy..claude/skills/pm-dispatch/SKILL.mdgains one line beside the four-piece-terminal trigger (line 608, 111 B): 「改动 >5000 行(含生成物)同换终局四件套,⛔ 无事实层例外;读数 = PR additions+deletions。」references/landing-operations.mdline 26 folds the size limb into the pre-check row, now spelled--pr N(which reads paths and size in one call), 117 B, ceiling unchanged at 69. The SKILL.md ceiling rises 812 → 813 inscripts/pm/check-skill-line-ratchet.mjsunder the ratchet's own maintainer exit, the ruling quoted in the entry (the 811 → 812 precedent's form).Readings — before / after, measured
43f476688)check-governed-merges.mjs --pr 18971(live API through the proxy)✅ NOT governed — ordinary queue landing applies⛔ HUMAN MERGE — 238429 changed line(s) (+238310 / -119) > 5000--pr 18994(2 files, +15 / −1)size: 16 changed line(s) (+15 / -1) ≤ 5000 — under the human-merge threshold--pr 18921(SKILL.md, +6 / −6)size: 12 changed line(s) … undercheck-governed-merges.mjs --self-testdispatch-gates.mjs --tier(no paths, this worktree)Changed lines — 722 (+691 / -31; generated files INCLUDED) vs the human-merge threshold 5000: under.dispatch-gates.mjs --tier packages/spec/src/index.tsChanged lines — NOT MEASURED: a path list carries no diff to count …check:pm-dispatch-gates(detached,tail --pid)43f476688)check:pm-skill-ratchetSelf-test pins on the threshold: exactly 5,000 changed lines is under; 5,001 is over; the +238,310 / −119 pair reads 238,429 and is over; a certified pure regeneration over the threshold still lands by a human merge; the verdict is byte-identical through
--branchand through--testonce the same list and numbers are handed in.Line budget (measured)
SKILL.md: 812 → 813 lines; ceiling 812 → 813 (maintainer exit). A fold was not available: 0 of 598 adjacent bullet pairs merge under the 120-byte cap (smallest 123 B); the trigger line (607) stands at 118 B; the rule's shortest self-contained form is 111 B; deleting a ruled clause is refused on the state-machine precedent.references/landing-operations.md: 69 → 69 lines (line 26: 118 B → 117 B).check:pm-skill-id-lint: 27 files clean (no issue-ID citation in either line).Gates (this head; exit codes captured before any pipe)
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackfrom the worktree at7fdd61ca0(42 commands; change set 5 paths, 724 changed lines by its own reading), every one run withcmd > log 2>&1; status=$?and reconciled with--ran:dispatch-gates --ran: 42 derived, 42 run, 0 NOT-MEASURED, 0 UNRUN (verdict line:✓ dispatch-gates --ran: 42 derived famil(ies) accounted for — 42 run, 0 NOT-MEASURED).check:pm-dispatch-gatesran detached (nohup+tail --pid, 795.6 s on this box):✓ dispatch-gates self-test: 1862 cases pass.check:pm-governed-merges:✓ check-governed-merges --self-test: 369 assertions.check:doc-formula-expressionsexited 3 (PREREQUISITE NOT MET:@objectstack/formula/@objectstack/lintnot built) on the first pass; both were built underscripts/pm/os-verify-lock.sh(VERDICT command-exit 0, 152 s held) and the gate reran green — the exit 3 was never a measurement.NOT MEASURED locally, by the derivation itself (CI-only, value-bearing argv):
scripts/check-shard-attestation.mjs --emit …,scripts/check-test-completeness.mjs …,scripts/pm/check-half-states.mjs --format=markdown --provenance=…; plus the 11 wide-population families and the 50 artifact-roster families CI runs on every PR, outside the derived total by design.pnpm lint(repo-wide eslint) is CI-owned and was not run here. No package build/test is owed: the diff touches nopackages/**file (no ①/② in the local verification scope), so the only lock-wrapped run was the formula/lint build above.Line-budget after the final commit (
7fdd61ca0):check:pm-skill-ratchet—.claude/skills/pm-dispatch/SKILL.md is 813 lines (ceiling 813; headroom 0),references/landing-operations.md is 69 lines (ceiling 69; headroom 0);check:pm-skill-id-lint— 27 file(s) clean.Deviations from the dispatch brief
--pr, 3 already means GOVERNED — the file's own rule is that no invocation carries both meanings — so a PR object without the pair is a REFUSAL on the derivation code 1 (a stated refusal, never 0, never a size of zero). The ruling's intent (never read as "not governed") is kept.--branchderives the size itself (--numstaton the range it lists) rather than taking passed-in numbers; the flags beside a deriving mode (--pr,--branch) are refused as two readings of one number, the way two mode flags are.dispatch-gates.mjs's self-test pins a NAMED census of live population markers by file and line; the import block moved this file's owninherited-populationmarker from :702 to :705, so that one row is updated — the census exists to be updated exactly this way.Acceptance notes
merge_groupleg reads the PATH register only; a seat that skips the landing pre-check can still enqueue an oversized PR. Dedupe words:queue guard size threshold,merge_group additions deletions,check-governed-queue-guard 5000,human merge line count.AGENTS.md green alone third class,5000 lines human merge AGENTS.check-governed-merges.mjs) lists governed-surface merges only; an oversized PR that landed through the queue is not listed. 承接者: the skills seat, together with the queue-guard follow-up above.check:doc-formula-expressionsexits 3 (PREREQUISITE NOT MET) on a fresh worktree until@objectstack/formulaand@objectstack/lintare built — by design of that gate; built under the verify lock here and rerun. 承接者: none.维护者速读(草稿)
改了什么:落地前检
check-governed-merges.mjs新增「体量」判据:PR 的 additions + deletions 超过 5000 行(含生成物)⇒ 只能人合,与受管面走同一终点;dispatch-gates在派发/认领时就把同一读数印在 tier 行旁;SKILL.md 与 landing-operations.md 各落一行规则。阈值只声明一次(HUMAN_MERGE_LINE_THRESHOLD = 5000),改它是一个词。为什么改:您 2026-09-18 的裁决。触发案例是 PR #18971(+238,310 / −119,其中 237,706 行是生成物)只凭 AI 审查就经队列合入;生成物不豁免,否则恰好豁免它。
风险与代价(含回滚):大 PR 的落地从「席位挂 auto-merge」变成「等您点一下」,每张超 5000 行的 PR 多一次人工动作;回滚 = revert 本 PR(纯脚本 + 两行规则文本,无发布物)。已知缺口:队列守卫的 merge_group 腿尚未读体量,眼下靠席位跑落地前检;已列为后续单。
席位意见:(留空)
你要做的:确认 5000 这个默认值(「比如」)是否就是您要的;是 ⇒ 人合本 PR;要改数字 ⇒ 说一个数即可。
Generated by Claude Code