Uh oh!
There was an error while loading. Please reload this page.
feat(ci): block a PR that declares itself only part of a card while also closing it - #8527
Merged
Merged
Conversation
The half-state sweep's H7 becomes a PR-scoped blocking check, reusing the sweep's exported predicate rather than forking it. H1-H6 stay report-only (they are facts about a shared board), and H7 stays in the sweep too for patrol coverage. Settles the fence question first, with one live controlled reading: fenced and inline closing keywords do not create a closing link, plain prose does, and the link exists at PR-open time rather than at merge. Recorded next to the inline-span measurement it extends. Part of the wiring: a standalone workflow subscribed to `edited` so a reworded body can go green without a push, and a lint.yml step that runs the new gate's self-test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018WuTtyckQa1VcXwgd52JpN
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
The job died in Setup Node.js, before the script ran: ##[error]Unable to locate executable file: pnpm. actions/setup-node@v5 defaults package-manager-cache: true, reads packageManager: pnpm@... out of package.json and shells out to pnpm to locate the store. This job installs no package manager on purpose, so pnpm is not on PATH. Nothing in the workflow source mentions pnpm, which is what made the first read of the error misleading. Now mirrors adr-merge-approval exactly -- checkout, setup-node@v7 with node-version '22', one node call, no install and no corepack -- which is the repo's known-good shape for a dependency-free check script. A new self-test case pins that the guard job names no package manager, so this cannot come back quietly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018WuTtyckQa1VcXwgd52JpN
hotlong
marked this pull request as ready for review
August 13, 2026 17:49
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#8476
Promotes the half-state sweep's H7 from a report-only patrol item to a PR-scoped blocking check — after settling, live, the one unknown its predicate carried.
Step 1 first: the fenced-code reading, settled
One controlled reading: three arms in ONE throwaway PR body, at one moment, with that PR open and unmerged (#8523, empty commit, never merged; it and its three targets are now shut).
closed_by_pull_requestson the targetRead seconds after that PR opened, and re-read unchanged ~15 minutes later. Two facts fall out:
stripMarkdownCode's fence rule is correct rather than merely reasonable — it is not a false negative, it needs no revision, and a blocking gate may rely on it.Arm C is what makes A and B readable at all. Without a positive control, "no link on the fenced arm" cannot be told apart from "closing links only materialize on merge".
Recorded in
scripts/pm/check-half-states.mjsbeside the inline-span reading it extends: in the header's reading list, and atstripMarkdownCodeitself, where the old "NOT independently measured" caveat stood.Step 2: the blocking check
scripts/check-partof-closing-keyword.mjs— a new, separate, PR-scoped gate. It fails a PR whose body says it is onlyPart ofsome card while also carrying a closing keyword bound to that same card.h7PartOfWithClosingKeywordfrom the sweep and reuses its finding sentence verbatim, so the gate's advice and the protocol's advice cannot drift. The sentence already carries the three approved rewordings ("card N is not addressed here" / "out of scope: card N" / put the keyword in backticks), and a self-test case pins each one.Part ofone card while genuinely closing another stays green, as does explanatory prose that puts a keyword in backticks.Wiring
A standalone workflow, modelled on the Duplicate Fix Guard — this repo's other PR-body-scoped blocking check — rather than a new job in the pr-automation family. Measured reasons:
editedis load-bearing. The body is the whole input, and the pr-automation family does not subscribe toedited; a re-run replays the frozen payload, so without it an author who reworded the sentence would stay red forever. Addingeditedto that shared trigger set would instead have made two label-writing jobs re-run on every body edit, which that file's header argues against at length.env:, never interpolated into arun:line — a PR body is arbitrary author-controlled text. A self-test case pins the spelling.actions/setup-node@v7pinned to Node 22, onenodecall. That is the ADR merge-approval gate's recipe, this repo's known-good shape for a dependency-free check script, and it is now matched exactly. This was measured the hard way — see the patch round below.merge_group:trigger: a queue event carries no pull request and so no body. That is also why this is not added to the required-context registry, whose entries must report on queue builds — and whose entries each carry a maintainer ruling. This publishes a red check run; whether it becomes a required context is a settings change no agent seat can make. The Duplicate Fix Guard sits in exactly the same position.lint.ymlruns the new gate's self-test unconditionally, following the PM dispatch-gates precedent: a self-test that runs only when someone remembers is a check whose coverage is a function of who remembered. That self-test also pins the wiring itself — the workflow still invokes the script, still subscribes to body edits, still passes the body throughenv:, and still names no package manager — so unwiring the gate reddens rather than going quiet.Error paths (the fail-open vs fail-closed call, stated)
Sibling checks in this repo treat an unread input as a failure, never a pass. This one splits the two cases that get conflated:
Part ofdeclaration.PR_NUMBERis the witness that separates it from "handed nothing", since GitHub renders a null body as an empty value.Patch round: the runtime step (head
2fe178e)The first head went red in CI, and the failure is worth recording because it names a tool this workflow never mentions:
The job died in Setup Node.js, before the script ran.
actions/setup-node@v5defaultspackage-manager-cache: true(the run log echoes it as a resolved input); that readspackageManager: pnpm@…out of package.json and shells out topnpmto locate the store to cache. This job installs no package manager on purpose, sopnpmis not on PATH and the action hard-errors. The misleading first read is that therun:line invoked a package manager — it did not, and never did; the reference is an implicit default, invisible in the workflow source.Fixed by matching the repo's own known-good recipe exactly (
setup-node@v7,node-version: '22', no corepack, no install). A new self-test case pins that the guard job names no package manager in any executable line, so this cannot return quietly.Verification
editedfrom the trigger reddens exactly the wiring case; reintroducing a package-manager invocation reddens exactly the new package-manager case; disabling fence-stripping in the shared predicate reddens exactly the fenced arm — and leaves the delegation case green, because both sides of that comparison call the same mutated predicate. The delegation case proves "no fork"; the fence case proves the rule.check:type-check-debtafter building the ledgered packages' dependency closure the waylint.ymldoes.✓ check:partof-closing-keyword: PR #8527 carries no Part-of/closing-keyword contradiction.Whole job, 8 seconds.No changeset: this PR ships CI tooling and releases nothing, so it carries
skip-changeset.Generated by Claude Code