Skip to content

Commit 9bfd6a6

Browse files
ci: fold the four PR-body guards into one pr-body-guards.yml job (2 of 3) (#26570)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8Bh55CNSFH3iN1HNN1Gog95)_ Part of #26071 Clause-②: no Metadata-protocol: none Ruling-ref: `Ruling: batch #397 item 2 · letter A · maintainer 「同意」` — comment 6416984681 on #26071 (director seat, 2026-10-26T13:18Z). This PR is (iii) of letter A, the second of three serial PRs from one card thread: (ii) the four daily patrols → `fleet-patrols.yml` (#26512, merged 2026-10-27T06:36Z as 3fcafcd), (iii) the four PR-body guards → one workflow (this), then (i) the `lint.yml` schedule skip. The card closes with the last one. Head branch `claude/project-thread-31axox-2` is the card thread's harness push branch with the `-2` the seat's claim (comment 6434226733) names for this PR; the harness push rule admits the thread's prefix. ## 维护者速读 - **改了什么**:四个 PR 正文守卫 workflow(closing-target-claim-guard、duplicate-fix-guard、partof-closing-keyword-guard、single-claim-path-guard)合为一个 `.github/workflows/pr-body-guards.yml`:一个 job、四个判定 step,每个 step 就是原文件的判定步骤(`env:`/`run:`/内联脚本逐字节相同,脚本比对见 Tests),step 名取原 job 的显示名;duplicate-fix 是内联 github-script,放在最前、checkout 之前;`merge_group` 触发保留,只有 closing-target 那步在队列构建上跑,其余三步只守 `pull_request`。按名读这些文件的五个门禁改指新文件,其中两个 step 名 pin 改为只读可执行行。 - **为什么改**:裁决 A 的 (iii)。四个文件各自 checkout + setup-node,每个 PR 事件起四个 run;合并后一次 checkout、一个 run,四个判定各自可见(每步 `if: !cancelled()`,前一步红不挡后面的判定)。 - **风险与回滚**:不改任何判定逻辑,`REQUIRED_CONTEXTS` 不动(四个守卫本来都不是 required context)。可见的变化三处:① Checks 页四个名字并成一个 `PR-body guards (…)`,各守卫的红落在 step 上;② `permissions` 取四者并集(全 read),三个脚本步骤的 token 比原来多一两个只读 scope;③ 裁决写的「三个守卫今天带 merge_group」在 main 上实际只有 closing-target 一个带,本 PR 按实情保留这一条腿(见 Acceptance notes)。回滚 = revert 本 PR,四个原文件原样回来。 - **先看**:① `pr-body-guards.yml` 的 `on:` 块与四个判定 step 的 `name:`/`if:`;② `scripts/check-closing-target-claim.mjs` 的 `CHECK_CONTEXT_NAME` 注释与 wiring 自检(改读去注释后的文本);③ `scripts/check-closing-keyword-parity.mjs` 的 PARSERS 登记行。 - **两分钟验证**:`pnpm check:closing-target-claim && pnpm check:partof-closing-keyword && pnpm check:single-claim-paths && node scripts/check-closing-keyword-parity.mjs` —— 四条 ✓/OK;PR 上的 `PR-body guards (…)` check 一个 run、四个判定 step 全绿。 - **需要你做的**:Approve(Tier H,`.github/workflows/**`);席位随后落地。 ## What changes File by file, against the retired workflows. ### `.github/workflows/pr-body-guards.yml` (new) ← `closing-target-claim-guard.yml`, `duplicate-fix-guard.yml`, `partof-closing-keyword-guard.yml`, `single-claim-path-guard.yml` (deleted) - **`on`** — `pull_request: types: [opened, edited, reopened, synchronize]`, identical in all four retired files, plus `merge_group:`, the leg `closing-target-claim-guard.yml` carried (the other three carried none — see Acceptance notes). No `paths:` filter, as in all four. - **`permissions`** — the union of the four retired grants, every scope `read`: `contents: read` (closing-target, partof, single-claim), `issues: read` (closing-target), `pull-requests: read` (closing-target, duplicate-fix, single-claim). Workflow-level, as each retired file declared its own. - **`concurrency`** — `pr-body-guards-${{ github.event.pull_request.number || github.ref }}`, `cancel-in-progress: true` (closing-target's shape, the one that covers the queue leg; on `merge_group` `github.ref` is the queue ref, unique per build, so no other PR's build is cancelled — which is what duplicate-fix's `cancel-in-progress: ${{ github.event_name == 'pull_request' }}` protected). - **One job `pr-body-guards`**, display name `PR-body guards (claim · duplicate · Part-of · single-writer path)`, `runs-on: ubuntu-latest`, `timeout-minutes: 10` (the largest of 10 / 5 / 5 / 5; the three measured walls — p99 0.3 / 0.8 / 0.8 min — are carried as comments at their steps). - **Six steps, in this order:** 1. `No other open PR may claim the same issue` — the duplicate-fix guard's github-script step, `if: ${{ !cancelled() && github.event_name == 'pull_request' }}`. FIRST, before any checkout: it needs none ("no checkout for the step that needs none"). 2. `Checkout repository` and 3. `Setup Node.js` — `if: ${{ !cancelled() }}`; taken from `partof-closing-keyword-guard.yml` (the three script-backed retired files carried these two steps byte-identically; the transport check compares all three). 4. `The card this PR closes must claim this branch` — closing-target, `if: ${{ !cancelled() }}`: runs on BOTH legs, as its file did. 5. `Part-of PR must not also close its card` — partof, `if: ${{ !cancelled() && github.event_name == 'pull_request' }}`. 6. `No other open PR may claim the same single-writer path` — single-claim, same `if:`. Each gate step is the retired gate step verbatim except its `name:` (now the retired JOB's display name, which was its check-run name, so the verdict reads in the step list as it read on the Checks tab) and the inserted `if:`; `env:` blocks, `run:` lines and the inline `script:` are unchanged. `!cancelled()` is what keeps four independent verdicts on one run: a step that failed does not hide the three others, as four jobs never did. - **Comments** — each retired file's header is carried whole above its step (headed `formerly .github/workflows/FILE`), and a new file header explains the one-job shape, the legs, the checkout order, the grants and which self-tests read the file. Seven comment edits beyond transport, each because the sentence described the retired one-file shape: the three "Sibling shape, deliberately copied" sentences (siblings are now steps of this workflow); single-claim's "is not touched by this file" → "this step" and "This workflow adds the second question" → "This step adds … as steps of one job"; in the shared setup-node comment, "The first draft of this file" → "of the Part-of guard's retired file", "the script is dependency-free and imports one sibling module" → "the three scripts are dependency-free and import at most two sibling modules", and "the known-good shape this now matches exactly: … one `node scripts/check-*.mjs` call" → "matches: … calls". The retired `on:` / `permissions:` / `concurrency:` / job comments are consolidated into the file-level comments, their load-bearing sentences kept (why `edited`, why `synchronize`, the multi-PR-group limit of the queue ref, why no `paths:`, why `contents: read`). - **History of the retired files**: GitHub keeps a deleted workflow's runs under its name (`GET /actions/workflows/FILE.yml` answers `state: deleted`); new history lands under `PR-Body Guards`. ### `scripts/check-closing-target-claim.mjs` - `WIRING_WORKFLOW` → `.github/workflows/pr-body-guards.yml`. `CHECK_CONTEXT_NAME` keeps its value; its docblock now says what it is: the gate STEP's `name:` in the wiring (the name its job carried as a check run until #26071), with the check-run name now the one job's. The wiring battery's name assertion reads the COMMENT-STRIPPED file and is relabelled — the new file's header quotes every step name in prose, so a raw-text read stayed green with the step renamed (measured, see Tests); the package-manager scan reuses the same stripped text. 121 cases, floors unchanged. ### `scripts/check-partof-closing-keyword.mjs` - `WIRING_WORKFLOW` → the new file; nothing else. 45 cases. ### `scripts/check-single-claim-paths.mjs` - `WIRING_WORKFLOW` → the new file; the sibling read (`siblingPath`, "the card-keyed duplicate gate still exists … and still asks its own question") → the same file, comment-stripped for the step-name pin, for the reason above. 93 cases. ### `scripts/check-closing-keyword-parity.mjs` - The `duplicate-fix-guard` registry entry: `file` → the new file, `job` → `pr-body-guards` (the extractor takes the named job's github-script step with a non-empty `script:`; the job has exactly one); the X2 mutation anchor's `file` likewise; four header sentences name the step instead of the file. 40 assertions, 5 mutations each driven to red; the sweep finds 6 files carrying the grammar across 21,437 tracked files, all registered. (The sweep reads `git ls-files`, so the new file must be tracked — it is staged; an untracked copy reads as "registered but no longer matches".) ### `scripts/pm/dispatch-gates.self-test.mjs` - The live env-carried specimen (`runCli(['--commands', '.github/workflows/partof-closing-keyword-guard.yml'])`) → the new file; the fixture's file label and the docblock sentence with it. The derivation on the new file satisfies the five assertions: `pnpm check:partof-closing-keyword` listed on stdout, `node scripts/check-partof-closing-keyword.mjs` absent from stdout and named `⊘ NOT MEASURED` on stderr, the accounting line present. **Not touched, by the order's surface:** `REQUIRED_CONTEXTS` (`ci.yml`, `lint.yml`, `governed-surface-guard.yml` only, as before); `lint.yml` (its step ids `single-claim-path-guard-self-test` / `closing-target-claim-guard-self-test` are step ids that run the scripts' self-tests, not file names, and `lint.yml` is PR 3's surface); `scripts/pm/check-expected-skips.mjs` (its `MEASURED_*` arrays record historical check-run names on dated run ids — fixtures, not the roster; the roster has no guard row); `docs/**` and `AGENTS.md` (no line names a guard FILE; `docs/audits/gate-census-2026-09.md` names the retired workflows' display names as a dated reading). ## Why The ruling (letter A, (iii)): one workflow, one job, four steps, keeping the `merge_group` trigger and no checkout for the step that needs none. This PR is that transport and nothing else — no judgement logic, no trigger, no grant beyond the union of what the four files already held. ## Tests **Transport proof** — a scratchpad script parsed `pr-body-guards.yml` and each retired file at `git show 9d3c156~1:.github/workflows/FILE` (this PR's parent, `ab3c776a`; the four blobs are identical to the ones at `3fcafcd3`, where the work started) with the `yaml` package: for every retired job, its gate step (minus the merged step's `if:`, with the retired step's `name:` restored, in the retired key order) deep-equals the merged step; the three retired checkout and setup-node steps each deep-equal the shared pair (minus `if:`); `on.pull_request.types` equal in all four; `merge_group` carried from closing-target and absent from the other three; no `paths:` on either side; every retired permission kept and the merged block exactly the union, all `read`; `runs-on` equal; `timeout-minutes` covers each retired value; the duplicate-fix step precedes the checkout; the six `if:` strings as listed; `on` has exactly `pull_request` + `merge_group`. Verdict: `✓ guards transport: 60 checks pass, 0 fail`. - `node scripts/check-closing-target-claim.mjs --self-test` → exit 0 — `✓ check-closing-target-claim self-test: 121 cases pass.` - `node scripts/check-partof-closing-keyword.mjs --self-test` → exit 0 — `✓ … 45 cases pass.` - `node scripts/check-single-claim-paths.mjs --self-test` → exit 0 — `✓ … 93 cases pass.` - `node scripts/check-closing-keyword-parity.mjs --self-test` → exit 0 — `40 assertions, 5 mutations of the shipped parsers each driven to red.`; `node scripts/check-closing-keyword-parity.mjs` → exit 0 — `OK (3 parsers agree on all 9 keywords and both measured separators; sweep found 6 file(s) carrying the grammar across 21437 tracked file(s), all registered).`; `--list` names `.github/workflows/pr-body-guards.yml` for `duplicate-fix-guard`. - **Reverse verification of the pins against the live file** (each mutation by `sed` on the new file; restore by `git checkout -- FILE` from the index, proven by `git hash-object` equality): rename the closing-target step → `check-closing-target-claim --self-test` exit 1; rename the duplicate-fix step → `check-single-claim-paths --self-test` exit 1; delete the `merge_group:` line → closing-target self-test exit 1; drop `edited` from the trigger types → partof self-test exit 1; narrow `const KEYWORDS` in the github-script step → parity gate exit 1; `pull-requests: read` → `none` → single-claim self-test exit 1. Measured on the way: the two step-RENAME mutations first stayed exit 0, because both pins read the raw file and the new header quotes every step name in prose — the phantom-pin hole the single-claim self-test already names for its permission reads. The two pins now read the comment-stripped text, and the renames red. - `node scripts/pm/dispatch-gates.mjs --commands .github/workflows/pr-body-guards.yml` → exit 0, 39 commands: `pnpm check:closing-target-claim`, `pnpm check:partof-closing-keyword`, `pnpm check:single-claim-paths` and both parity invocations listed; `scripts/check-closing-target-claim.mjs`, `check-partof-closing-keyword.mjs`, `check-single-claim-paths.mjs` each `⊘ NOT MEASURED` on stderr (env-carried), none offered bare on stdout. - Not owed: no package `test` / `typecheck` (the diff touches no package source); `test:repo`, `pnpm lint` and the whole-tree scans are CI's. Dogfood battery: n/a — no `!` in the title and no `(narrowing)` arm. No ablation beyond the six mutations: no tested behaviour changes. ## Acceptance notes - **Premise, corrected on the tree:** the ruling and the order say the merged workflow keeps "the `merge_group` trigger the three guards carry today (`closing-target-claim-guard`, `partof-closing-keyword-guard`, `single-claim-path-guard`)". On `origin/main` — at the seat's premise read `d0a7e748`, at `3fcafcd3` where this work started, and at this PR's base `ab3c776a` alike — only `closing-target-claim-guard.yml` carries `merge_group:`; the other two say in their headers "No `merge_group:` trigger, and that is not an oversight: a merge-queue event carries no pull request and therefore no body". The letter is kept: the merged workflow subscribes `merge_group`, the closing-target step runs on the queue build, and the three steps whose retired files never ran there are gated to the `pull_request` leg by `if:`. No guard gains or loses a leg. Reported as the premise deviation in the os-dev report. - **The behaviour changes of one job, stated:** (1) one check-run name, `PR-body guards (claim · duplicate · Part-of · single-writer path)`, where the Checks tab showed four — each guard's verdict is its step, named as the job was; nothing pinned the four names as required contexts (`REQUIRED_CONTEXTS` unchanged, and `check-expected-skips`' roster has no guard row). (2) `permissions` is the union: the partof step's token now also carries `issues: read` and `pull-requests: read`, the single-claim and duplicate-fix steps' `issues: read` — all read, no write anywhere, as before. (3) Every gate step runs under `!cancelled()`, so a run shows all four verdicts, as four independent jobs did; the job is red if any step is. (4) The duplicate-fix guard runs before the checkout, in the same job; the three script-backed steps share one checkout + setup-node instead of three. (5) `timeout-minutes: 10` for the whole job (was 10 / 5 / 5 / 5 per job). (6) On a `merge_group` run the duplicate-fix, partof and single-claim steps read as skipped steps of a green-or-red job, where before their workflows simply did not run. - **Branch naming:** this PR's head is `claude/project-thread-31axox-2`, as the seat's claim (comment 6434226733) names it. The claim names the third branch only by its suffix. The closing-target claim guard on PR 3, the PR that will carry the card's closing keyword, reads the newest `Claim:` for the FULL head branch name, so the seat may want a claim line naming `claude/project-thread-31axox-3` in full before PR 3 opens. Flagged in the hand-back. - **Stale mentions left in place** (comment-only, outside the order's surface; carrier: the seat's stale-name sweep card from #26512 — same class): `.github/workflows/pr-automation.yml:18-19`, `cross-repo-issue-closer.yml:216,227`, `.github/actions/setup-workspace/action.yml:85`, `lint.yml:1076,1082` (step ids, not file names), `scripts/check-changeset-no-major.mjs:1047`, `scripts/check-cross-repo-closer-outcome.mjs:1105`, `scripts/check-empty-changeset.mjs:42` (a changeset name), `scripts/check-pnpm-acquisition.mjs:65`, `scripts/pm/check-half-states.mjs:6272`, `scripts/pm/dispatch-gates.mjs:935,4379` (docblocks naming the measured specimen). The parity gate's `duplicate-fix-guard` parser ID is an ID, kept. `docs/audits/gate-census-2026-09.md` is a dated census and is left as the reading it was. - **`actionlint`** is not wired in this repository; the workflow was parsed with the `yaml` package by the transport check and by the three wiring batteries and the parity extractor. - **Seat: no claim amendment** — the surface is the four files plus the five gates the order's own `git grep` finds reading them by name, and nothing else. ## Verification Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on head 9d3c156 (10 paths vs merge base ab3c776; 1,082 changed lines, under the 3,000 human-merge threshold): 51 families. The work started on `3fcafcd3`, where the same 51 ran green; `origin/main` then moved 280 files (among them `lint.yml`, `package.json`, `dispatch-gates.data.mjs` and one hunk of `dispatch-gates.self-test.mjs` that does not overlap this PR's), so the commit was rebased onto `ab3c776a`, re-derived (the identical 51) and every family re-run on the rebased head. The readings below are the rebased head's. Each ran in this worktree under `scripts/pm/os-verify-lock.sh` (slot `issue-26071`) with the exit code captured before any pipe (`bash -c "$cmd" > log 2>&1; st=$?`), then reconciled: - `node scripts/pm/dispatch-gates.mjs --ran ran.list --repo objectstack-ai/objectstack` → exit 0 — `✓ dispatch-gates --ran: 51 derived famil(ies) accounted for — 51 run, 0 NOT-MEASURED (a DERIVED zero — all 51 recorded an exit code and none of them is 3).` - `pnpm check:pm-dispatch-gates` → exit 0 — `✓ dispatch-gates self-test (fast tier): 1935 cases pass; 7 slow section(s) deferred.` (441 s on this shared box). - `pnpm check:closing-target-claim`, `pnpm check:partof-closing-keyword`, `pnpm check:single-claim-paths`, `node scripts/check-closing-keyword-parity.mjs` and its `--self-test` → exit 0 each (in the list below). - The whole batch ran under `scripts/pm/os-verify-lock.sh` (slot `issue-26071`): `VERDICT command-exit 0 · held the lock 1135s`. - `actionlint`: not wired in this repository. - `REQUIRED_CONTEXTS` untouched: `scripts/check-required-contexts.mjs` still names only `ci.yml`, `lint.yml` and `governed-surface-guard.yml`. - CI on the PR is not waited for (the order forbids it); the seat reads the check-runs. Every reading above was taken on head 9d3c156; no commit followed. <details><summary>All 51 derived families, exit 0 each (wall seconds shown where ≥ 10 s)</summary> - `node scripts/check-closing-keyword-parity.mjs` → exit 0 (27 s) - `node scripts/check-closing-keyword-parity.mjs --self-test` → exit 0 - `node scripts/check-comment-mask-corpus.mjs` → exit 0 (180 s) - `node scripts/check-declaration-mirrors.mjs` → exit 0 - `node scripts/check-declaration-mirrors.mjs --self-test` → exit 0 - `node scripts/check-self-test-wired.mjs` → exit 0 - `node scripts/check-self-test-wired.mjs --self-test` → exit 0 - `node scripts/check-self-test-workflow-commands.mjs` → exit 0 (134 s) - `node scripts/check-self-test-workflow-commands.mjs --self-test` → exit 0 - `node scripts/check-step-collectors.mjs` → exit 0 - `node scripts/check-step-collectors.mjs --self-test` → exit 0 - `node scripts/check-whole-set-label-write.mjs` → exit 0 - `node scripts/check-whole-set-label-write.mjs --self-test` → exit 0 - `node scripts/ci/plan-test-core-shards.mjs --self-test` → exit 0 - `node scripts/docs-audit/check-drift-comment.mjs` → exit 0 - `node scripts/pm/bare-root-worklist.mjs --self-test` → exit 0 (37 s) - `node scripts/pm/ci-failure.mjs --self-test` → exit 0 - `pnpm check:agent-test-spelling` → exit 0 - `pnpm check:aggregator-roster` → exit 0 - `pnpm check:bash32-floor` → exit 0 - `pnpm check:ci-filter-parity` → exit 0 - `pnpm check:cli-command-ids` → exit 0 (19 s) - `pnpm check:closing-target-claim` → exit 0 - `pnpm check:cross-package-test-inputs` → exit 0 (43 s) - `pnpm check:declared-population-live` → exit 0 (42 s) - `pnpm check:driver-memory-census` → exit 0 - `pnpm check:entry-guard` → exit 0 (54 s) - `pnpm check:gitlink-declared` → exit 0 - `pnpm check:node-version` → exit 0 - `pnpm check:nul-bytes` → exit 0 - `pnpm check:parse-guard` → exit 0 - `pnpm check:partof-closing-keyword` → exit 0 - `pnpm check:pm-dispatch-gates` → exit 0 (441 s) - `pnpm check:pm-expected-skips` → exit 0 - `pnpm check:pm-governed-merges` → exit 0 - `pnpm check:pm-post-stamped` → exit 0 - `pnpm check:pm-record-recognisers` → exit 0 - `pnpm check:pnpm-acquisition` → exit 0 - `pnpm check:pnpm-filter-targets` → exit 0 - `pnpm check:pnpm-flag-spellings` → exit 0 - `pnpm check:ratchet-remedy-authority` → exit 0 - `pnpm check:refd-timer-probe` → exit 0 (25 s) - `pnpm check:required-contexts` → exit 0 - `pnpm check:scripts-symbol-anchors` → exit 0 - `pnpm check:shard-attestation` → exit 0 - `pnpm check:single-claim-paths` → exit 0 - `pnpm check:stall-guard-budget` → exit 0 - `pnpm check:stall-guard-headroom` → exit 0 - `pnpm check:watch-hint-literal` → exit 0 - `pnpm check:workflow-status-functions` → exit 0 - `pnpm check:workflow-step-name-quoting` → exit 0 </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbW8nFvKeVjgKv8RTwH)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 833e4a8 commit 9bfd6a6

10 files changed

Lines changed: 542 additions & 540 deletions

‎.github/workflows/closing-target-claim-guard.yml‎

Lines changed: 0 additions & 140 deletions
This file was deleted.

‎.github/workflows/duplicate-fix-guard.yml‎

Lines changed: 0 additions & 156 deletions
This file was deleted.

0 commit comments

Comments
 (0)