Skip to content

Commit be49304

Browse files
os-warrenclaude
andauthored
feat(pm): give check-half-states a standing caller — scheduled patrol workflow + markdown anchor report (#9862)
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 Claude-Session: https://claude.ai/code/session_01AeA3nU1B5Q2pgxqxgUrexd Co-authored-by: Claude <noreply@anthropic.com>
1 parent 597a466 commit be49304

2 files changed

Lines changed: 579 additions & 10 deletions

File tree

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
name: Half-State Patrol
2+
3+
# The standing caller for `scripts/pm/check-half-states.mjs` (#9844).
4+
#
5+
# ## Why a workflow, and not "a seat should run it"
6+
#
7+
# The sweeper carries thirteen predicates over the dispatch protocol's
8+
# label/assignee/PR invariants, and for most of its life its documented consumer
9+
# was "a PM seat's patrol round" — which is to say, nobody's calendar. A shift
10+
# covering two lanes declared a queue empty from memory while eight malformed
11+
# claims (H2) and an unenumerated backlog sat on the board. Not one predicate had
12+
# fired. A healing mechanism with no scheduled caller heals only in the
13+
# counterfactual, and an alarm added to a script nobody runs is still silence.
14+
#
15+
# "Some seat should run it" also kept not happening for a MEASURED reason, not a
16+
# discipline one: the live sweep cannot run inside a PM session container at all
17+
# (#7412 class 1 — api.github.com refuses that egress in both directions, with and
18+
# without a token). The fix therefore had to move the caller somewhere the
19+
# transport prerequisite is actually met. A GitHub Actions runner with the
20+
# workflow's own `GITHUB_TOKEN` is that place — #7412 class 2, the triage Routine
21+
# container, is the same shape and measured reachable with 15,000 core quota.
22+
#
23+
# ## What lands where
24+
#
25+
# One pinned ANCHOR ISSUE, rewritten in place every run (`ANCHOR_ISSUE` below).
26+
# Never a comment per run: the board is one board, a per-run comment stream would
27+
# be a second tracker that nobody prunes, and GitHub's edit history is already the
28+
# archive this needs. The body is owned end-to-end by the generator, so no run can
29+
# leave half of it stale.
30+
#
31+
# The `Swept` timestamp in that body is the patrol's heartbeat and is deliberately
32+
# refreshed even when the findings are unchanged: a timestamp that stops advancing
33+
# is how a reader learns the standing caller died. That is the whole defect class
34+
# this workflow exists to close, so the run must not "optimize away" the no-op
35+
# edit that proves it is alive.
36+
#
37+
# ## Report-only, and the one thing that is NOT report-only
38+
#
39+
# Findings never fail anything. A completed sweep exits 0 whether it found 0 or 40
40+
# half-states, this job never writes a label, never closes a card, never fixes a
41+
# state, and no H-predicate is a blocking gate — the script's own header argues
42+
# that at length (a half-state is a fact about a live shared board, not about
43+
# whichever PR happens to run CI next).
44+
#
45+
# The job DOES fail when the sweep could not run, or when its report could not be
46+
# delivered. That is not a gate on the board; it is the patrol reporting its own
47+
# death. A workflow that quietly does nothing because a credential lapsed is the
48+
# exact shape this repo keeps having to fix (#4449, #9575), and it is doubly
49+
# unacceptable here: silent non-delivery would leave a stale anchor body that
50+
# reads exactly like a clean board — the #4690 failure ("could not read the input"
51+
# must never look like "input is clean") with a timestamp on it. Failing costs
52+
# nobody a PR: this workflow gates no branch and blocks no queue.
53+
54+
on:
55+
schedule:
56+
# Four times a day, six hours apart, at :37 past the hour.
57+
#
58+
# The minute is offset ON PURPOSE. The triage Routine that heals these same
59+
# states fires hourly near the top of the hour, and a patrol landing at the
60+
# same minute would keep reading the board mid-heal — reporting half-states
61+
# the healer is in the middle of pairing, i.e. manufacturing findings that
62+
# clear themselves. :37 puts this sweep in the quiet part of the healer's
63+
# cycle in both directions. Four runs/day rather than hourly: H13's own
64+
# threshold is 2h and the incident it comes from sat ~26h, so six-hourly
65+
# detection is two orders of magnitude better than the status quo (never)
66+
# while staying cheap on the core quota this sweep shares with the loop's
67+
# hot path.
68+
- cron: '37 1,7,13,19 * * *'
69+
workflow_dispatch: {}
70+
# Changes to the patrol itself get exercised before they merge — the same
71+
# posture as engine-split-metric.yml. On a pull_request run the sweep still
72+
# executes (that is the point: the transport, the flags and the rendering are
73+
# proven on a real runner), but the anchor write is skipped and the rendered
74+
# body goes to the run's step summary instead. A PR must never rewrite the
75+
# board's pinned view.
76+
pull_request:
77+
paths:
78+
- 'scripts/pm/check-half-states.mjs'
79+
- '.github/workflows/half-state-patrol.yml'
80+
81+
# Least privilege: this job reads the repo and writes exactly one issue BODY.
82+
# `issues: write` is the narrowest scope GitHub offers for that edit; the job
83+
# never uses it for labels, comments, assignees or state, and the sweeper it
84+
# calls is read-only against the API by construction.
85+
permissions:
86+
contents: read
87+
issues: write
88+
89+
# One patrol at a time. A scheduled run overlapping a manual dispatch would have
90+
# two runs racing to rewrite the same body, and the loser's findings would vanish
91+
# with no trace but an edit-history entry.
92+
concurrency:
93+
group: half-state-patrol
94+
cancel-in-progress: false
95+
96+
env:
97+
# The pinned anchor issue whose body this workflow owns.
98+
#
99+
# TO ROTATE: open a new `tracking`-labeled issue, put its number here, and note
100+
# the handover in the OLD issue's body before closing it (its edit history is
101+
# the archive and does not travel). Nothing else reads this number, so the
102+
# rotation is this one line.
103+
#
104+
# The anchor deliberately carries `tracking` and NO `domain:*` label: `tracking`
105+
# is in the sweeper's own H13_EXEMPT_LABELS, so the anchor can never appear as a
106+
# finding in the sweep it hosts.
107+
ANCHOR_ISSUE: '9857'
108+
109+
jobs:
110+
patrol:
111+
name: Live half-state sweep
112+
runs-on: ubuntu-latest
113+
timeout-minutes: 15
114+
steps:
115+
- name: Checkout repository
116+
uses: actions/checkout@v7
117+
118+
- name: Setup Node.js
119+
uses: actions/setup-node@v7
120+
with:
121+
node-version: '22'
122+
123+
# No `pnpm install`: the sweeper imports nothing but `node:process` and
124+
# global `fetch`. Installing the workspace here would buy nothing and would
125+
# give a scheduled patrol a lockfile it could fail on.
126+
- name: Run the live sweep
127+
id: sweep
128+
env:
129+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
130+
PROVENANCE: >-
131+
run [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
132+
· commit `${{ github.sha }}` · trigger `${{ github.event_name }}`
133+
run: |
134+
set +e
135+
node scripts/pm/check-half-states.mjs \
136+
--format=markdown \
137+
--provenance="$PROVENANCE" \
138+
> "$RUNNER_TEMP/report.md" 2> "$RUNNER_TEMP/report.err"
139+
code=$?
140+
set -e
141+
# Captured with NO pipe in between. `cmd | tail` would report the
142+
# PIPE's status — `tail` essentially never fails, so a green and a red
143+
# sweep both read as 0, and the script's own header calls this trap out
144+
# by name (its exit codes are 0 / 2 / 3 and the split is the point).
145+
echo "exit_code=$code" >> "$GITHUB_OUTPUT"
146+
echo "check-half-states exited $code"
147+
cat "$RUNNER_TEMP/report.err" >&2 || true
148+
149+
- name: Update the pinned anchor issue
150+
# A pull_request run proves the sweep; it must not touch the board.
151+
if: github.event_name != 'pull_request'
152+
uses: actions/github-script@v9
153+
env:
154+
SWEEP_EXIT: ${{ steps.sweep.outputs.exit_code }}
155+
with:
156+
# Delivery is retried, never assumed (#9575): this single PATCH is the
157+
# entire product of the run, and a transient answer from the issues
158+
# endpoint would otherwise discard a completed sweep.
159+
retries: 3
160+
script: |
161+
const fs = require('fs');
162+
const path = require('path');
163+
const exitCode = Number(process.env.SWEEP_EXIT);
164+
const anchor = Number(process.env.ANCHOR_ISSUE);
165+
const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
166+
const read = (name) => {
167+
try { return fs.readFileSync(path.join(process.env.RUNNER_TEMP, name), 'utf8'); }
168+
catch { return ''; }
169+
};
170+
171+
// The composition split, deliberately: a COMPLETED sweep renders its
172+
// own body (in the script, where --self-test pins every property of
173+
// it). Only the did-not-run body is composed here, because saying
174+
// "my callee failed" is the caller's job and the script's classified
175+
// output is already the authored explanation — this wraps it, it
176+
// does not re-word it.
177+
let body;
178+
if (exitCode === 0) {
179+
body = read('report.md');
180+
if (!body.trim()) {
181+
throw new Error('the sweep exited 0 but produced an empty report — refusing to blank the anchor');
182+
}
183+
} else {
184+
const classified = (read('report.err') || read('report.md') || '(no output captured)').trim();
185+
const kind = exitCode === 3
186+
? 'PREREQUISITE NOT MET — the runner could not reach the board'
187+
: 'SWEEP FAILED — an unclassified failure';
188+
body = [
189+
'os-half-state-sweep — machine-findable marker for this generated view.',
190+
'',
191+
`# ⛔ THE SWEEP DID NOT RUN (exit ${exitCode})`,
192+
'',
193+
`_Attempted ${new Date().toISOString()} · [run log](${runUrl}) · ${kind}._`,
194+
'',
195+
'Nothing below is a finding. **No issue was judged**, so this body says nothing about whether',
196+
'the board carries half-states — it is not a clean board and it is not a dirty one, it is no',
197+
'reading at all. A sweep that could not run must never read as a clean board.',
198+
'',
199+
'The standing patrol is DOWN until this is fixed; the previous run\'s findings are in this',
200+
'issue\'s edit history. The sweeper\'s own classified output:',
201+
'',
202+
'```',
203+
classified,
204+
'```',
205+
].join('\n');
206+
}
207+
208+
await github.rest.issues.update({
209+
owner: context.repo.owner,
210+
repo: context.repo.repo,
211+
issue_number: anchor,
212+
body,
213+
});
214+
core.info(`anchor #${anchor} updated (${body.length} chars, sweep exit ${exitCode})`);
215+
216+
- name: Publish the rendered body to the run summary
217+
# Always: on a PR this IS the delivery, and on a scheduled run it makes
218+
# the run log self-contained when someone opens it after an alert.
219+
if: always()
220+
run: |
221+
{
222+
echo "### Half-state patrol — sweep exit ${{ steps.sweep.outputs.exit_code }}"
223+
echo
224+
if [ "${{ github.event_name }}" = "pull_request" ]; then
225+
echo "_Anchor write skipped: a pull_request run proves the sweep without touching the board._"
226+
echo
227+
fi
228+
echo '<details><summary>Rendered anchor body</summary>'
229+
echo
230+
cat "$RUNNER_TEMP/report.md" 2>/dev/null || echo '(no report produced)'
231+
echo
232+
echo '</details>'
233+
echo
234+
echo '<details><summary>stderr</summary>'
235+
echo
236+
echo '```'
237+
cat "$RUNNER_TEMP/report.err" 2>/dev/null || true
238+
echo '```'
239+
echo
240+
echo '</details>'
241+
} >> "$GITHUB_STEP_SUMMARY"
242+
243+
- name: Fail the run if the sweep could not run
244+
# LAST, on purpose: the anchor is updated with the did-not-run report
245+
# BEFORE the job goes red. Land the truth, then raise the alarm — a run
246+
# that failed early would leave the previous body in place with its old
247+
# timestamp, which is precisely the stale-reads-as-clean shape above.
248+
#
249+
# Findings are NOT a failure condition and never appear here: exit 0 with
250+
# 40 half-states is a successful patrol.
251+
if: steps.sweep.outputs.exit_code != '0'
252+
run: |
253+
echo "::error::check-half-states exited ${{ steps.sweep.outputs.exit_code }} — the standing patrol did not read the board. See the anchor issue and this run's stderr."
254+
exit 1

0 commit comments

Comments
 (0)