Repository navigation
Commit 9bfd6a6
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
File tree
- .github/workflows
- scripts
- pm
This file was deleted.
This file was deleted.
0 commit comments