Repository navigation
ci: fold the four PR-body guards into one pr-body-guards.yml job (2 of 3) - #26570
Conversation
closing-target-claim-guard, duplicate-fix-guard, partof-closing-keyword-guard and single-claim-path-guard become four steps of one job in .github/workflows/pr-body-guards.yml. Each gate step is the retired step verbatim but for its name (the retired job's display name) and an `if:`: every step runs under !cancelled(), so one run still shows four verdicts. The duplicate-fix github-script step runs first, before the one checkout + setup-node the three script-backed steps share. The merge_group leg stays for the closing-target step, the only retired file that carried it; the other three steps are gated to pull_request. Permissions are the union of the four retired grants, all read. The gates that read those files by name follow: the WIRING_WORKFLOW of check-closing-target-claim, check-partof-closing-keyword and check-single-claim-paths, the keyword-parity registry entry and mutation anchor, and the dispatch-gates self-test's live env-carried specimen. The two step-name pins now read the comment-stripped file, since the new header quotes every step name in prose. Claude-Session: https://claude.ai/code/session_01JQwxbW8nFvKeVjgKv8RTwH Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Director seat (summon #52, ledger objectstack#12708), after the fact by the maintainer's word 「#26566、#26568、#26570、#26585 也要追加复审,虽然我已经点了合并」: approved by ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Retracted: this comment was a duplicate post of the director seat's contract-review record on this PR; the record of record is comment 6439741369 above (same head, same verdict). Nothing else changes. Generated by Claude Code |
Requested by Warren · project thread
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) thelint.ymlschedule skip. The card closes with the last one. Head branchclaude/project-thread-31axox-2is the card thread's harness push branch with the-2the seat's claim (comment 6434226733) names for this PR; the harness push rule admits the thread's prefix.维护者速读
.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 改为只读可执行行。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 全绿。.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, plusmerge_group:, the legclosing-target-claim-guard.ymlcarried (the other three carried none — see Acceptance notes). Nopaths:filter, as in all four.permissions— the union of the four retired grants, every scoperead: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; onmerge_groupgithub.refis the queue ref, unique per build, so no other PR's build is cancelled — which is what duplicate-fix'scancel-in-progress: ${{ github.event_name == 'pull_request' }}protected).pr-body-guards, display namePR-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).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").Checkout repositoryand 3.Setup Node.js—if: ${{ !cancelled() }}; taken frompartof-closing-keyword-guard.yml(the three script-backed retired files carried these two steps byte-identically; the transport check compares all three).The card this PR closes must claim this branch— closing-target,if: ${{ !cancelled() }}: runs on BOTH legs, as its file did.Part-of PR must not also close its card— partof,if: ${{ !cancelled() && github.event_name == 'pull_request' }}.No other open PR may claim the same single-writer path— single-claim, sameif:.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 insertedif:;env:blocks,run:lines and the inlinescript: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.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: … onenode scripts/check-*.mjscall" → "matches: … calls". The retiredon:/permissions:/concurrency:/ job comments are consolidated into the file-level comments, their load-bearing sentences kept (whyedited, whysynchronize, the multi-PR-group limit of the queue ref, why nopaths:, whycontents: read).GET /actions/workflows/FILE.ymlanswersstate: deleted); new history lands underPR-Body Guards.scripts/check-closing-target-claim.mjsWIRING_WORKFLOW→.github/workflows/pr-body-guards.yml.CHECK_CONTEXT_NAMEkeeps its value; its docblock now says what it is: the gate STEP'sname:in the wiring (the name its job carried as a check run until [Decision] Scheduled and bot workflows: the 4-hourly lint run repeats the push run, four daily patrols are four workflows, four PR-body guards are four runners — consolidate, and to what cadence #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.mjsWIRING_WORKFLOW→ the new file; nothing else. 45 cases.scripts/check-single-claim-paths.mjsWIRING_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.mjsduplicate-fix-guardregistry entry:file→ the new file,job→pr-body-guards(the extractor takes the named job's github-script step with a non-emptyscript:; the job has exactly one); the X2 mutation anchor'sfilelikewise; 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 readsgit 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.mjsrunCli(['--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-keywordlisted on stdout,node scripts/check-partof-closing-keyword.mjsabsent from stdout and named⊘ NOT MEASUREDon stderr, the accounting line present.Not touched, by the order's surface:
REQUIRED_CONTEXTS(ci.yml,lint.yml,governed-surface-guard.ymlonly, as before);lint.yml(its step idssingle-claim-path-guard-self-test/closing-target-claim-guard-self-testare step ids that run the scripts' self-tests, not file names, andlint.ymlis PR 3's surface);scripts/pm/check-expected-skips.mjs(itsMEASURED_*arrays record historical check-run names on dated run ids — fixtures, not the roster; the roster has no guard row);docs/**andAGENTS.md(no line names a guard FILE;docs/audits/gate-census-2026-09.mdnames the retired workflows' display names as a dated reading).Why
The ruling (letter A, (iii)): one workflow, one job, four steps, keeping the
merge_grouptrigger 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.ymland each retired file atgit show 9d3c1566~1:.github/workflows/FILE(this PR's parent,ab3c776a; the four blobs are identical to the ones at3fcafcd3, where the work started) with theyamlpackage: for every retired job, its gate step (minus the merged step'sif:, with the retired step'sname: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 (minusif:);on.pull_request.typesequal in all four;merge_groupcarried from closing-target and absent from the other three; nopaths:on either side; every retired permission kept and the merged block exactly the union, allread;runs-onequal;timeout-minutescovers each retired value; the duplicate-fix step precedes the checkout; the sixif:strings as listed;onhas exactlypull_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).;--listnames.github/workflows/pr-body-guards.ymlforduplicate-fix-guard.sedon the new file; restore bygit checkout -- FILEfrom the index, proven bygit hash-objectequality): rename the closing-target step →check-closing-target-claim --self-testexit 1; rename the duplicate-fix step →check-single-claim-paths --self-testexit 1; delete themerge_group:line → closing-target self-test exit 1; dropeditedfrom the trigger types → partof self-test exit 1; narrowconst KEYWORDSin 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-pathsand both parity invocations listed;scripts/check-closing-target-claim.mjs,check-partof-closing-keyword.mjs,check-single-claim-paths.mjseach⊘ NOT MEASUREDon stderr (env-carried), none offered bare on stdout.test/typecheck(the diff touches no package source);test:repo,pnpm lintand 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
merge_grouptrigger the three guards carry today (closing-target-claim-guard,partof-closing-keyword-guard,single-claim-path-guard)". Onorigin/main— at the seat's premise readd0a7e748, at3fcafcd3where this work started, and at this PR's baseab3c776aalike — onlyclosing-target-claim-guard.ymlcarriesmerge_group:; the other two say in their headers "Nomerge_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 subscribesmerge_group, the closing-target step runs on the queue build, and the three steps whose retired files never ran there are gated to thepull_requestleg byif:. No guard gains or loses a leg. Reported as the premise deviation in the os-dev report.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_CONTEXTSunchanged, andcheck-expected-skips' roster has no guard row). (2)permissionsis the union: the partof step's token now also carriesissues: readandpull-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: 10for the whole job (was 10 / 5 / 5 / 5 per job). (6) On amerge_grouprun 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.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 newestClaim:for the FULL head branch name, so the seat may want a claim line namingclaude/project-thread-31axox-3in full before PR 3 opens. Flagged in the hand-back..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'sduplicate-fix-guardparser ID is an ID, kept.docs/audits/gate-census-2026-09.mdis a dated census and is left as the reading it was.actionlintis not wired in this repository; the workflow was parsed with theyamlpackage by the transport check and by the three wiring batteries and the parity extractor.git grepfinds reading them by name, and nothing else.Verification
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon head 9d3c156 (10 paths vs merge base ab3c776; 1,082 changed lines, under the 3,000 human-merge threshold): 51 families. The work started on3fcafcd3, where the same 51 ran green;origin/mainthen moved 280 files (among themlint.yml,package.json,dispatch-gates.data.mjsand one hunk ofdispatch-gates.self-test.mjsthat does not overlap this PR's), so the commit was rebased ontoab3c776a, 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 underscripts/pm/os-verify-lock.sh(slotissue-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.mjsand its--self-test→ exit 0 each (in the list below).scripts/pm/os-verify-lock.sh(slotissue-26071):VERDICT command-exit 0 · held the lock 1135s.actionlint: not wired in this repository.REQUIRED_CONTEXTSuntouched:scripts/check-required-contexts.mjsstill names onlyci.yml,lint.ymlandgoverned-surface-guard.yml.All 51 derived families, exit 0 each (wall seconds shown where ≥ 10 s)
node scripts/check-closing-keyword-parity.mjs→ exit 0 (27 s)node scripts/check-closing-keyword-parity.mjs --self-test→ exit 0node scripts/check-comment-mask-corpus.mjs→ exit 0 (180 s)node scripts/check-declaration-mirrors.mjs→ exit 0node scripts/check-declaration-mirrors.mjs --self-test→ exit 0node scripts/check-self-test-wired.mjs→ exit 0node scripts/check-self-test-wired.mjs --self-test→ exit 0node scripts/check-self-test-workflow-commands.mjs→ exit 0 (134 s)node scripts/check-self-test-workflow-commands.mjs --self-test→ exit 0node scripts/check-step-collectors.mjs→ exit 0node scripts/check-step-collectors.mjs --self-test→ exit 0node scripts/check-whole-set-label-write.mjs→ exit 0node scripts/check-whole-set-label-write.mjs --self-test→ exit 0node scripts/ci/plan-test-core-shards.mjs --self-test→ exit 0node scripts/docs-audit/check-drift-comment.mjs→ exit 0node scripts/pm/bare-root-worklist.mjs --self-test→ exit 0 (37 s)node scripts/pm/ci-failure.mjs --self-test→ exit 0pnpm check:agent-test-spelling→ exit 0pnpm check:aggregator-roster→ exit 0pnpm check:bash32-floor→ exit 0pnpm check:ci-filter-parity→ exit 0pnpm check:cli-command-ids→ exit 0 (19 s)pnpm check:closing-target-claim→ exit 0pnpm check:cross-package-test-inputs→ exit 0 (43 s)pnpm check:declared-population-live→ exit 0 (42 s)pnpm check:driver-memory-census→ exit 0pnpm check:entry-guard→ exit 0 (54 s)pnpm check:gitlink-declared→ exit 0pnpm check:node-version→ exit 0pnpm check:nul-bytes→ exit 0pnpm check:parse-guard→ exit 0pnpm check:partof-closing-keyword→ exit 0pnpm check:pm-dispatch-gates→ exit 0 (441 s)pnpm check:pm-expected-skips→ exit 0pnpm check:pm-governed-merges→ exit 0pnpm check:pm-post-stamped→ exit 0pnpm check:pm-record-recognisers→ exit 0pnpm check:pnpm-acquisition→ exit 0pnpm check:pnpm-filter-targets→ exit 0pnpm check:pnpm-flag-spellings→ exit 0pnpm check:ratchet-remedy-authority→ exit 0pnpm check:refd-timer-probe→ exit 0 (25 s)pnpm check:required-contexts→ exit 0pnpm check:scripts-symbol-anchors→ exit 0pnpm check:shard-attestation→ exit 0pnpm check:single-claim-paths→ exit 0pnpm check:stall-guard-budget→ exit 0pnpm check:stall-guard-headroom→ exit 0pnpm check:watch-hint-literal→ exit 0pnpm check:workflow-status-functions→ exit 0pnpm check:workflow-step-name-quoting→ exit 0Generated by Claude Code