Skip to content

fix(pm): H19's summary clause ends its own sentence #93

fix(pm): H19's summary clause ends its own sentence

fix(pm): H19's summary clause ends its own sentence #93

name: Half-State Patrol
# The standing caller for `scripts/pm/check-half-states.mjs` (#9844).
#
# ## Why a workflow, and not "a seat should run it"
#
# The sweeper carries thirteen predicates over the dispatch protocol's
# label/assignee/PR invariants, and for most of its life its documented consumer
# was "a PM seat's patrol round" — which is to say, nobody's calendar. A shift
# covering two lanes declared a queue empty from memory while eight malformed
# claims (H2) and an unenumerated backlog sat on the board. Not one predicate had
# fired. A healing mechanism with no scheduled caller heals only in the
# counterfactual, and an alarm added to a script nobody runs is still silence.
#
# "Some seat should run it" also kept not happening for a MEASURED reason, not a
# discipline one: in every container class measured at the time, the live sweep
# could not run at all (#7412 class 1 — api.github.com refuses that egress in
# both directions, with and without a token). The fix therefore had to move the
# caller somewhere the transport prerequisite is actually met. A GitHub Actions
# runner with the workflow's own `GITHUB_TOKEN` is that place — #7412 class 2,
# the triage Routine container, is the same shape and measured reachable with
# 15,000 core quota.
#
# ⚠️ CORRECTED (#13544, measured 2026-08-31): "cannot run inside an agent
# container at all" is no longer true of every such container, and the claim
# above is kept only as the history that put this workflow here. A proxied agent
# container reaches api.github.com fully — `/rate_limit`, `/user` AND
# `GET /repos/{owner}/{repo}` all 200 with `server: github.com` — once node's
# fetch is routed through the session proxy, which the script now does for
# itself. What had actually failed was the ROUTE: node's `fetch` ignores
# `HTTPS_PROXY`, so the sweeper sent the proxy's placeholder token straight to
# GitHub, earned a 401, and reported that refusal as the container's verdict.
#
# ⛔ That does NOT retire this workflow, and the fix deliberately did not touch
# it. The #9844 reason stands on its own and is not a transport reason: an alarm
# whose only caller is "a seat should remember" is silence, whoever CAN run it.
# The on-demand path is restored BESIDE the schedule — a lane that needs the
# board read right now (the 4x/day body trims its own rows, and says so) can now
# get it — never instead of the schedule.
#
# What this means for a runner is nothing at all, and that is load-bearing given
# this file is copied VERBATIM into sibling repos (#11217): the script re-execs
# only when `HTTPS_PROXY`/`https_proxy` is set, and a runner sets neither. No
# child process, no extra line, no behaviour change — pinned in the script's
# `--self-test` and measured byte-identical on the probe output.
#
# ## What lands where
#
# One pinned ANCHOR ISSUE, rewritten in place every run (`ANCHOR_ISSUE` below).
# Never a comment per run: the board is one board, a per-run comment stream would
# be a second tracker that nobody prunes, and GitHub's edit history is already the
# archive this needs. The body is owned end-to-end by the generator, so no run can
# leave half of it stale.
#
# The `Swept` timestamp in that body is the patrol's heartbeat and is deliberately
# refreshed even when the findings are unchanged: a timestamp that stops advancing
# is how a reader learns the standing caller died. That is the whole defect class
# this workflow exists to close, so the run must not "optimize away" the no-op
# edit that proves it is alive.
#
# ## Report-only, and the one thing that is NOT report-only
#
# Findings never fail anything. A completed sweep exits 0 whether it found 0 or 40
# half-states, this job never writes a label, never closes a card, never fixes a
# state, and no H-predicate is a blocking gate — the script's own header argues
# that at length (a half-state is a fact about a live shared board, not about
# whichever PR happens to run CI next).
#
# The job DOES fail when the sweep could not run, or when its report could not be
# delivered. That is not a gate on the board; it is the patrol reporting its own
# death. A workflow that quietly does nothing because a credential lapsed is the
# exact shape this repo keeps having to fix (#4449, #9575), and it is doubly
# unacceptable here: silent non-delivery would leave a stale anchor body that
# reads exactly like a clean board — the #4690 failure ("could not read the input"
# must never look like "input is clean") with a timestamp on it. Failing costs
# nobody a PR: this workflow gates no branch and blocks no queue.
#
# ## Adopting this in a sibling repo (#11217)
#
# This file is REPO-AGNOSTIC and is meant to be copied verbatim. It was not:
# installed in objectstack alone, it left 37 of the fleet's 59 open `pm:blocked`
# cards outside any patrol, and a hand-run of H19's predicate over objectui's
# blocked inventory found 7 blocks whose blocker had already closed — 58% of
# that repo's machine-readable blocks were false, one of them for a week. The
# same predicate had been catching objectstack's four every hour. The difference
# was never discipline; it was that one repo had a caller.
#
# To adopt, in the sibling repo:
#
# 1. copy `scripts/pm/check-half-states.mjs` and this file, unchanged;
# 2. open one `tracking`-labeled anchor issue there and set the repository
# VARIABLE `HALF_STATE_ANCHOR_ISSUE` to its number
# (Settings → Secrets and variables → Actions → Variables).
#
# That is the whole install. The swept repo needs no configuration at all: it is
# `github.repository`, so the copy reads the board it lives in — a hardcoded
# default was how a copied file could have swept THIS repo and written the
# findings into a sibling's anchor, a fully green report about the wrong board.
#
# ⛔ Each install uses its OWN `secrets.GITHUB_TOKEN` and reads its own repo. No
# cross-repo credential, no matrix over repos, no PAT: that route was refused at
# grading (it buys no coverage a per-repo install lacks and raises the
# credential floor for every repo at once). The accepted consequence is that a
# cross-repo `Blocked-by:` target stays UNJUDGED in each install — H19 says so
# in its own row rather than reading it as a healthy block.
on:
schedule:
# Four times a day, six hours apart, at :37 past the hour.
#
# The minute is offset ON PURPOSE. The triage Routine that heals these same
# states fires hourly near the top of the hour, and a patrol landing at the
# same minute would keep reading the board mid-heal — reporting half-states
# the healer is in the middle of pairing, i.e. manufacturing findings that
# clear themselves. :37 puts this sweep in the quiet part of the healer's
# cycle in both directions. Four runs/day rather than hourly: H13's own
# threshold is 2h and the incident it comes from sat ~26h, so six-hourly
# detection is two orders of magnitude better than the status quo (never)
# while staying cheap on the core quota this sweep shares with the loop's
# hot path.
- cron: '37 1,7,13,19 * * *'
workflow_dispatch: {}
# Changes to the patrol itself get exercised before they merge — the same
# posture as engine-split-metric.yml. On a pull_request run the sweep still
# executes (that is the point: the transport, the flags and the rendering are
# proven on a real runner), but the anchor write is skipped and the rendered
# body goes to the run's step summary instead. A PR must never rewrite the
# board's pinned view.
pull_request:
paths:
- 'scripts/pm/check-half-states.mjs'
- '.github/workflows/half-state-patrol.yml'
# Least privilege: this job reads the repo and writes exactly one issue BODY.
# `issues: write` is the narrowest scope GitHub offers for that edit; the job
# never uses it for labels, comments, assignees or state, and the sweeper it
# calls is read-only against the API by construction.
permissions:
contents: read
issues: write
# One patrol at a time. A scheduled run overlapping a manual dispatch would have
# two runs racing to rewrite the same body, and the loser's findings would vanish
# with no trace but an edit-history entry.
concurrency:
group: half-state-patrol
cancel-in-progress: false
env:
# The pinned anchor issue whose body this workflow owns — the ONE per-repo
# input this file takes (#11217).
#
# Resolution: the repository variable `HALF_STATE_ANCHOR_ISSUE` if set, else
# this repo's own pinned number, else EMPTY — and empty makes the job refuse
# to write rather than guess (see the "Resolve the anchor" step). The literal
# is guarded by the repository name on purpose: an unguarded fallback is what
# would let a verbatim copy in objectui rewrite ITS #9857 — some unrelated
# card — with this board's findings, silently and four times a day. A number
# is only ever meaningful in the repo it was minted in.
#
# TO ROTATE (here): open a new `tracking`-labeled issue, put its number below,
# and note the handover in the OLD issue's body before closing it (its edit
# history is the archive and does not travel).
# TO ADOPT (a sibling repo): change NOTHING here — set the repository variable.
#
# The anchor deliberately carries `tracking` and NO `domain:*` label: `tracking`
# is in the sweeper's own H13_EXEMPT_LABELS, so the anchor can never appear as a
# finding in the sweep it hosts.
#
# ⚠️ Folded scalar, and every continuation line sits at the SAME indent on
# purpose: a more-indented line in a `>-` block keeps its newline literally
# (measured on this very value), which would hand the expression parser a
# multi-line string instead of one expression.
ANCHOR_ISSUE: >-
${{ vars.HALF_STATE_ANCHOR_ISSUE
|| (github.repository == 'objectstack-ai/objectstack' && '9857')
|| '' }}
jobs:
patrol:
name: Live half-state sweep
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
# No `pnpm install`: the sweeper imports nothing but `node:process` and
# global `fetch`. Installing the workspace here would buy nothing and would
# give a scheduled patrol a lockfile it could fail on.
- name: Run the live sweep
id: sweep
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# WHICH board this run reads: the repo this workflow is installed in,
# always. The sweeper would resolve the same answer on its own from
# the runner's `GITHUB_REPOSITORY` (`resolveSweepRepo`), and it is
# passed explicitly anyway so the wiring is visible to a reader of the
# workflow — the two agree by construction and a copy of this file
# cannot end up sweeping the repo it was copied FROM.
PM_SWEEP_REPO: ${{ github.repository }}
PROVENANCE: >-
run [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
· commit `${{ github.sha }}` · trigger `${{ github.event_name }}`
run: |
set +e
node scripts/pm/check-half-states.mjs \
--format=markdown \
--provenance="$PROVENANCE" \
> "$RUNNER_TEMP/report.md" 2> "$RUNNER_TEMP/report.err"
code=$?
set -e
# Captured with NO pipe in between. `cmd | tail` would report the
# PIPE's status — `tail` essentially never fails, so a green and a red
# sweep both read as 0, and the script's own header calls this trap out
# by name (its exit codes are 0 / 2 / 3 and the split is the point).
echo "exit_code=$code" >> "$GITHUB_OUTPUT"
echo "check-half-states exited $code"
cat "$RUNNER_TEMP/report.err" >&2 || true
- name: Resolve the anchor issue
# An install with no anchor configured has nowhere to land its report,
# and the ONLY safe behaviour is to say so loudly (#11217). The two
# alternatives are both the failure this file exists to prevent:
# guessing a number would rewrite an unrelated card in this repo, and
# skipping the write quietly would leave a patrol that runs, finds, and
# tells nobody — indistinguishable from a clean board.
#
# Placed AFTER the sweep so the run summary still carries the rendered
# findings (the same "land the truth, then raise the alarm" order the
# final step keeps), and skipped on a pull_request run, which never
# writes an anchor at all.
if: github.event_name != 'pull_request'
run: |
if [ -z "${ANCHOR_ISSUE//[[:space:]]/}" ]; then
echo "::error::No anchor issue configured for ${{ github.repository }}. The sweep RAN (see the run summary) but has nowhere to land. Open a \`tracking\`-labeled anchor issue in this repo and set the repository variable HALF_STATE_ANCHOR_ISSUE to its number (Settings -> Secrets and variables -> Actions -> Variables)."
exit 1
fi
case "$ANCHOR_ISSUE" in
*[!0-9]*|'') echo "::error::HALF_STATE_ANCHOR_ISSUE is '$ANCHOR_ISSUE', which is not an issue number."; exit 1 ;;
esac
echo "anchor: #$ANCHOR_ISSUE in ${{ github.repository }}"
- name: Update the pinned anchor issue
# A pull_request run proves the sweep; it must not touch the board.
if: github.event_name != 'pull_request'
uses: actions/github-script@v9
env:
SWEEP_EXIT: ${{ steps.sweep.outputs.exit_code }}
with:
# Delivery is retried, never assumed (#9575): this single PATCH is the
# entire product of the run, and a transient answer from the issues
# endpoint would otherwise discard a completed sweep.
retries: 3
script: |
const fs = require('fs');
const path = require('path');
const exitCode = Number(process.env.SWEEP_EXIT);
const anchor = Number(process.env.ANCHOR_ISSUE);
const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
const read = (name) => {
try { return fs.readFileSync(path.join(process.env.RUNNER_TEMP, name), 'utf8'); }
catch { return ''; }
};
// The composition split, deliberately: a COMPLETED sweep renders its
// own body (in the script, where --self-test pins every property of
// it). Only the did-not-run body is composed here, because saying
// "my callee failed" is the caller's job and the script's classified
// output is already the authored explanation — this wraps it, it
// does not re-word it.
let body;
if (exitCode === 0) {
body = read('report.md');
if (!body.trim()) {
throw new Error('the sweep exited 0 but produced an empty report — refusing to blank the anchor');
}
} else {
const classified = (read('report.err') || read('report.md') || '(no output captured)').trim();
const kind = exitCode === 3
? 'PREREQUISITE NOT MET — the runner could not reach the board'
: 'SWEEP FAILED — an unclassified failure';
body = [
'os-half-state-sweep — machine-findable marker for this generated view.',
'',
`# ⛔ THE SWEEP DID NOT RUN (exit ${exitCode})`,
'',
`_Attempted ${new Date().toISOString()} · [run log](${runUrl}) · ${kind}._`,
'',
'Nothing below is a finding. **No issue was judged**, so this body says nothing about whether',
'the board carries half-states — it is not a clean board and it is not a dirty one, it is no',
'reading at all. A sweep that could not run must never read as a clean board.',
'',
'The standing patrol is DOWN until this is fixed; the previous run\'s findings are in this',
'issue\'s edit history. The sweeper\'s own classified output:',
'',
'```',
classified,
'```',
].join('\n');
}
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: anchor,
body,
});
core.info(`anchor #${anchor} updated (${body.length} chars, sweep exit ${exitCode})`);
- name: Publish the rendered body to the run summary
# Always: on a PR this IS the delivery, and on a scheduled run it makes
# the run log self-contained when someone opens it after an alert.
if: always()
run: |
{
echo "### Half-state patrol — sweep exit ${{ steps.sweep.outputs.exit_code }}"
echo
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "_Anchor write skipped: a pull_request run proves the sweep without touching the board._"
echo
fi
echo '<details><summary>Rendered anchor body</summary>'
echo
cat "$RUNNER_TEMP/report.md" 2>/dev/null || echo '(no report produced)'
echo
echo '</details>'
echo
echo '<details><summary>stderr</summary>'
echo
echo '```'
cat "$RUNNER_TEMP/report.err" 2>/dev/null || true
echo '```'
echo
echo '</details>'
} >> "$GITHUB_STEP_SUMMARY"
- name: Fail the run if the sweep could not run
# LAST, on purpose: the anchor is updated with the did-not-run report
# BEFORE the job goes red. Land the truth, then raise the alarm — a run
# that failed early would leave the previous body in place with its old
# timestamp, which is precisely the stale-reads-as-clean shape above.
#
# Findings are NOT a failure condition and never appear here: exit 0 with
# 40 half-states is a successful patrol.
if: steps.sweep.outputs.exit_code != '0'
run: |
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."
exit 1