Uh oh!
There was an error while loading. Please reload this page.
feat(pm): give check-half-states a standing caller — scheduled patrol workflow + pinned anchor report - #9862
Merged
Conversation
… workflow + markdown anchor report The sweeper carried thirteen predicates and no calendar: its documented consumer was "a PM seat's patrol round", and the live sweep cannot run inside a PM session container at all (#7412 class 1). So it watched nothing. - `.github/workflows/half-state-patrol.yml`: scheduled 4x/day at :37 (offset from the hourly triage Routine so the patrol never reads the board mid-heal), runs the live sweep on a runner where the transport prerequisite is met, and rewrites ONE pinned anchor issue in place. Never a comment per run; edit history is the archive. - `--format=markdown` / `--provenance=` on the sweeper: the anchor body is rendered by the script, where --self-test pins it. Loud H13 P0-SUSPECT rows sort above the fold and out of truncation's reach; the plain terminal output is byte-identical to before. Report-only stays report-only: no H-predicate becomes a gate, no label is ever written, findings never fail the run. The job fails only when the sweep could not RUN or its report could not be delivered — a stale anchor reads exactly like a clean board, which is the failure this card exists to end. Part of #9844 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeA3nU1B5Q2pgxqxgUrexd
os-warren
marked this pull request as ready for review
August 19, 2026 06:13
Contributor
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32222566532 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
github-merge-queueBot
removed this pull request from the merge queue due to failed status checks
Aug 19, 2026
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 19, 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.
Part of #9844
What this is
scripts/pm/check-half-states.mjscarries thirteen predicates over the dispatch protocol's label/assignee/PR invariants, and its documented consumer was "a PM seat's patrol round" — nobody's calendar. Eight malformed claims (H2) and an unenumerated two-lane backlog sat on the board until the maintainer asked; not one predicate had fired. A healing mechanism with no scheduled caller heals only in the counterfactual."Some seat should run it" also kept not happening for a measured reason rather than a discipline one: the live sweep cannot run inside a PM session container at all (#7412 class 1 — api.github.com refuses that egress both with and without a token). So the caller had to move somewhere the transport prerequisite is actually met.
This PR gives the sweeper a standing caller: a scheduled GitHub Actions workflow that runs the live sweep with the workflow's own
GITHUB_TOKENand lands the result by rewriting one pinned anchor issue in place.Files
.github/workflows/half-state-patrol.yml(new) — cron37 1,7,13,19 * * *(four times a day), plusworkflow_dispatchand apull_requesttrigger scoped to this workflow and the sweeper.scripts/pm/check-half-states.mjs—--format=markdownand--provenance=, an exportedP0_SUSPECT_MARKER, and 40 new self-test cases. No predicate changed; no H-behaviour moved.tracking-labeled protocol carrier, body owned by the workflow. Its number is hardcoded in the workflow'sANCHOR_ISSUEenv var with a comment naming how to rotate it. It still needs a human pin — the title asks for it.The premise is confirmed on a real runner
The load-bearing assumption of this card — that an Actions runner is a place where the live sweep CAN run, unlike every agent container measured in #7412 — is no longer an assumption. This PR's own
pull_requestrun executed it:Job: Live half-state sweep — conclusion success, 23s wall time for the whole live sweep. So on a runner: the transport prerequisite is met,
--format=markdownrenders, and the anchor write was correctly skipped because the event waspull_request(the step summary carries the rendered body instead, with the "Anchor write skipped" notice). The only path not exercised by that run is the anchor PATCH itself, which fires onschedule/workflow_dispatch.That
pull_requesttrigger is in the workflow precisely so this evidence exists before merge, and it keeps existing for every future edit to the patrol.Design decisions worth reviewing
Where the report is composed. A completed sweep renders its own markdown body inside the script, where
--self-testcan pin every property of it. Only the "did not run" body is composed in the workflow's inline step, because saying "my callee failed" is the caller's job and the script's classified stderr is already the authored explanation — the step wraps it, it does not re-word it.Why the markdown renderer sorts rows differently from the plain one. The plain output goes to a terminal, which has no fold; it keeps its existing issue-number order and is byte-identical to before (proved, see below). The markdown output goes into an issue body, which is read at a fold and trimmed at a hard 65536-char cap. A P0-SUSPECT row at position 38 of 40, or trimmed off the end, is exactly the silence this card exists to end — so loud rows sort first and are therefore the last thing truncation could ever reach. Within each band issue-number order is preserved, so the anchor's edit history stays diffable.
The heartbeat. The
Swepttimestamp is refreshed on every run even when findings are unchanged. That no-op edit is not waste: a timestamp that stops advancing is the only way a reader learns the standing caller died, which is this card's entire defect class. The workflow must not optimize it away.Report-only, and the one thing that is not. No H-predicate becomes a gate, no label is ever written, no state is ever fixed, and findings never fail the run — exit 0 with 40 half-states is a successful patrol. The job fails only when the sweep could not RUN (exit 3 or 2) or when its report could not be delivered. That is not a gate on the board; it is the patrol reporting its own death. Silent non-delivery would leave a stale anchor body reading exactly like a clean board — the #4690 shape with a timestamp on it. Failing costs nobody a PR: this workflow gates no branch and blocks no queue. The anchor is updated with the did-not-run report before the job goes red, so the truth lands first.
The cron minute is offset on purpose. The triage Routine that heals these same states fires hourly near the top of the hour. A patrol landing on that minute would keep reading the board mid-heal and manufacture findings that clear themselves.
:37sits in the quiet part of the healer's cycle in both directions.Least privilege.
contents: read+issues: write, which is the narrowest scope GitHub offers for a body edit. The job never uses it for labels, comments, assignees or state. Aconcurrencygroup keeps a manual dispatch from racing a scheduled run over the same body.The anchor cannot pollute its own sweep. It carries
trackingand deliberately nodomain:*label, andtrackingis already in the sweeper'sH13_EXEMPT_LABELS.Gates run — all on the final commit
6fc4612e1node scripts/pm/dispatch-gates.mjs(no paths; derived from the real diff — 2 paths, 113 families discovered, 11 matched):pnpm check:pm-half-statescheck-half-states self-test: 194 cases pass.pnpm check:cross-package-test-inputsOK: 12 package(s) read outside themselves, all declared…pnpm check:node-versionOK (29 setup-node step(s) across 26 workflow(s), all on Node 22).pnpm check:partof-closing-keywordcheck-partof-closing-keyword self-test: 28 cases pass.pnpm check:required-contextspnpm check:shard-attestation2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).pnpm check:workflow-status-functionsOK (scanned 26 workflow file(s), 45 job(s)…)node scripts/check-nul-bytes.mjsOK (scanned 6282 text file(s)… no raw ASCII control bytes).The guard was also run against this PR body before the PR was opened —
PR_BODY=… node scripts/check-partof-closing-keyword.mjs→✓ this PR carries no Part-of/closing-keyword contradiction.Workflow validation: actionlint 1.7.7 exits 0 on this file and on all 26 workflows; every
run:block alsobash -nparses, and the inline github-script body passesnode --check. (shellcheck is absent from this container, so actionlint's shell-lint pass did not run —bash -nis the substitute, and CI's own actionlint is the authority.)Two extra verifications beyond the gate set:
renderPlain:identical: true | bytes before/after: 393 393. The terminal report did not move.Residuals, stated honestly
pull_requestrun proves the sweep and the skip; the write path fires first on the initialscheduleor a manualworkflow_dispatch. A dispatch run after merge is the cheapest way to confirm it, and [Half-state patrol] check-half-states live sweep — generated view (please pin) #9857 will show the result.--probereturns exit 3,PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential, with 28 requests left on a shared anonymous per-egress-IP quota ([finding]scripts/pm/check-half-states.mjslive sweep cannot run in a PM session container — direct api.github.com is 403 with or without the token, and the docblock's "unauthenticated works" is false here #7412 class 3). No sweep was attempted from here; a shared quota would have been spent on a run guaranteed to die mid-way.Out of scope, as the card requires
No H-predicate was turned into a blocking gate, and the workflow was not taught to mutate labels or states.
Generated by Claude Code