Skip to content

feat(pm): give check-half-states a standing caller — scheduled patrol workflow + pinned anchor report - #9862

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-9844-half-state-patrol-caller
Aug 19, 2026
Merged

feat(pm): give check-half-states a standing caller — scheduled patrol workflow + pinned anchor report#9862
os-warren merged 1 commit into
mainfrom
claude/issue-9844-half-state-patrol-caller

Conversation

@os-warren

@os-warrenos-warren commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Part of #9844

What this is

scripts/pm/check-half-states.mjs carries 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_TOKEN and lands the result by rewriting one pinned anchor issue in place.

Files

  • .github/workflows/half-state-patrol.yml (new) — cron 37 1,7,13,19 * * * (four times a day), plus workflow_dispatch and a pull_request trigger scoped to this workflow and the sweeper.
  • scripts/pm/check-half-states.mjs--format=markdown and --provenance=, an exported P0_SUSPECT_MARKER, and 40 new self-test cases. No predicate changed; no H-behaviour moved.
  • Anchor issue [Half-state patrol] check-half-states live sweep — generated view (please pin) #9857 — created as a tracking-labeled protocol carrier, body owned by the workflow. Its number is hardcoded in the workflow's ANCHOR_ISSUE env 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_request run executed it:

PROVENANCE: run [32220278666](…/actions/runs/32220278666) · commit `7d054b52b…` · trigger `pull_request`
check-half-states exited 0

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=markdown renders, and the anchor write was correctly skipped because the event was pull_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 on schedule / workflow_dispatch.

That pull_request trigger 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-test can 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 Swept timestamp 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. :37 sits 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. A concurrency group keeps a manual dispatch from racing a scheduled run over the same body.

The anchor cannot pollute its own sweep. It carries tracking and deliberately no domain:* label, and tracking is already in the sweeper's H13_EXEMPT_LABELS.

Gates run — all on the final commit 6fc4612e1

node scripts/pm/dispatch-gates.mjs (no paths; derived from the real diff — 2 paths, 113 families discovered, 11 matched):

gateverdict line
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-contextsexit 0
pnpm 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 also bash -n parses, and the inline github-script body passes node --check. (shellcheck is absent from this container, so actionlint's shell-lint pass did not run — bash -n is the substitute, and CI's own actionlint is the authority.)

Two extra verifications beyond the gate set:

  1. Plain-output byte parity. A harness rendered the same findings through a verbatim copy of the pre-change inline printer and through renderPlain: identical: true | bytes before/after: 393 393. The terminal report did not move.
  2. The inline workflow script driven against mocks, all four branches — exit 0 passes the script-rendered body through verbatim; exit 3 and exit 2 produce the did-not-run body with the right classification and the script's stderr embedded; and exit 0 with an empty report throws instead of blanking the anchor. 10/10 assertions.

Residuals, stated honestly

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

… 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
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32222566532 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (3/3) — 失败步骤: Run this shard's tests(日志不可读,点进 job 看)

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 7 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 19, 2026
@os-warren
os-warren added this pull request to the merge queueAug 19, 2026
Merged via the queue into main with commit be49304Aug 19, 2026
31 checks passed
@os-warren
os-warren deleted the claude/issue-9844-half-state-patrol-caller branch August 19, 2026 07:12
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-warren@claude