Skip to content

Commit 5e3caa1

Browse files
claude[bot]claude
andauthored
Widen the half-state patrol to the states the board actually produces: H24/H25/H26, sibling-repo adoption, and re-derived windows (#11294)
* pm(patrol): H24 queue+assignee, the pm:awaiting-maintainer state, and the paired-write remedies (#11196) The two-views contradiction gets a row of its own: an open card carrying `pm:queue` with a non-empty assignee is dispatchable to the queue view and taken to the claim rule at the same time, so nobody can legally move it. Pure intersection of two fields — no threshold, no identity test — because the field carries both dead agent claims and genuine human ownership and the maintainer's ruling puts the rule first, with any ownership exemption as an explicit marker later. The row names the login and states the asymmetric remedy instead. H8's landing re-label and H19's unlock scan are the two rollback paths that never named the assignee drop (only dead-claim reclamation did), so both remedy sentences now say the paired write includes it. The half-delivered H8 branch deliberately does not: there the label and the claim are correct. `pm:awaiting-maintainer` is implemented as a first-class state per the ruling of 2026-08-23 — vocabulary entry in ensure-pm-labels.sh, H25's exclusivity against every other state label, and membership in H11's parked inventory, H13's state vocabulary and H22's residue set, so no reader is left with a four-row hole. Its SKILL.md state-model row and its application to the specimen card are deliberately deferred, not dropped. Self-test 879 -> 931 cases, both directions pinned for every new predicate and every vocabulary membership. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ * pm(patrol): parameterise the sweeper/workflow pair so a sibling repo adopts it verbatim (#11217) The patrol was installed in objectstack alone, which left 37 of the fleet's 59 open `pm:blocked` cards outside any machine sweep — a hand-run of H19's predicate over objectui found 7 blocks whose blocker had already closed, 58% of that repo's machine-readable blocks, one of them stale for a week. The same predicate catches objectstack's every hour. Two hardcodings made "copy the pair verbatim" unsafe, and both are removed: * the swept repo now resolves PM_SWEEP_REPO -> GITHUB_REPOSITORY -> default, so a copy reads the board it lives in instead of this one. A malformed value is refused at the CLI (exit 2) rather than silently replaced by the default: substituting a different board is the failure being closed. * the anchor issue comes from the repository variable HALF_STATE_ANCHOR_ISSUE, with this repo's 9857 kept only as a fallback GUARDED by the repository name. An install with no anchor configured fails loudly after the sweep instead of rewriting whatever #9857 happens to be in that repo. The objectstack leg is unchanged by construction: its runner sets GITHUB_REPOSITORY (and now PM_SWEEP_REPO) to the same string the hardcoded default carried, and the guarded fallback resolves to the same 9857 — pinned in the self-test in exactly the shape the runner provides. Per-repo installs with each repo's own GITHUB_TOKEN, per the grading ruling; no cross-repo credential. Self-test 931 -> 951 cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ * pm(patrol): H26 — name the block whose target can never close, and the transitive wait (#11219) The unlock predicate is "the `Blocked-by:` target CLOSED", and `pm:on-hold` / `needs-user-decision` are by definition states a card sits in WHILE OPEN. A block naming such a target is structurally indefinite and nothing reported it: the waiting card is perfectly well-formed — line present (H4), target resolves and is open (H19), label correct — and H9 audits the HELD card, not the waiting one. Six measured instances, every one found by a human reading, including one repo whose entire blocked inventory (2 of 2) waits on its single unanswered decision card. A second leg on the same data flags a target that is itself `pm:blocked`: the measured chain was real one hop up and false two hops up, and a single-level predicate cannot see that. It names the hop rather than chasing it. Quota-neutral: H19 already resolves every distinct target, from a listing in hand or with one GET, and a resolved target's LABELS rode in on a payload already paid for — the resolution rows simply stop discarding them. Report-only, and explicitly not a claim that the block is wrong; sometimes waiting on a deferred card is right. Deliberately not reported: a target labelled `pm:queue` while titled `[Decision]` — a mislabelling, not a fact in the labels. Self-test 951 -> 977 cases, both directions pinned for each leg, including the disjointness from H19 (a closed target is H19's row, an unresolved one is H19's sentence) and the case where both fire on one card. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ * pm(patrol): re-derive H8/H22/H23's window arithmetic against the measured merge rate (#11118) H8's docblock justified a two-page window with "~18 merges/day", a figure the repo had outrun more than sevenfold. The sentence claimed a reach "well past the longest measured unexecuted-verdict latency" (~11 days at that rate); the window it described had become 1.78 days. Nothing could notice, because the arithmetic was prose. Re-measured 2026-08-23T08:42:15Z, windows pinned as full ISO instants per the card's approxidate warning: rate 300 commits, 2026-08-21T04:00:19Z..2026-08-23T08:22:47Z = ~137.5/day (300/300 carrying the squash marker; agrees with the card's ~132/day) H8 2 pages = 1.78d (oldest merge 2026-08-21T14:00:28Z) 4 pages = 2.96d (oldest merge 2026-08-20T09:41:02Z) -> widened H22 2 pages = 0.65d of update-recency (~15.6 HOURS, ~2.6 sweeps) 4 pages = 1.70d -> widened H23 3 pages = 2.18d, ~8.7 sweeps -> unchanged H8 is widened rather than restated because its damage model is asymmetric: the residue it reports is the paired write nobody noticed, which correlates with age, so the population most likely to age out is the population the row is for. H22's surprise is the divisor — ordered by `updated`, its rows are consumed by issue ACTIVITY, not closures, and the rate is bursty in exactly the direction that ejects fresh residue while residue is being produced fastest. Four pages restores what its anti-drowning argument was choosing (a couple of days) and does not reopen the deep tail it refuses (500+ carriers spanning months). H23 kept its cap: it is the one window that DERIVED its number from a measured rate, and the only one that survived a re-measure. The derivation is now executable — `windowCoverageDays`, `sweepOverlap`, `MEASURED_MERGES_PER_DAY`, `PATROL_CADENCE_HOURS` — so a cap and the sentence justifying it cannot drift apart again. Cost: four extra requests per sweep, four sweeps a day, against a 15,000/h quota. Self-test 977 -> 997 cases, including the degenerate inputs that must answer "cannot divide" rather than Infinity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9e72090 commit 5e3caa1

3 files changed

Lines changed: 1058 additions & 35 deletions

File tree

‎.github/workflows/half-state-patrol.yml‎

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,35 @@ name: Half-State Patrol
5050
# reads exactly like a clean board — the #4690 failure ("could not read the input"
5151
# must never look like "input is clean") with a timestamp on it. Failing costs
5252
# nobody a PR: this workflow gates no branch and blocks no queue.
53+
#
54+
# ## Adopting this in a sibling repo (#11217)
55+
#
56+
# This file is REPO-AGNOSTIC and is meant to be copied verbatim. It was not:
57+
# installed in objectstack alone, it left 37 of the fleet's 59 open `pm:blocked`
58+
# cards outside any patrol, and a hand-run of H19's predicate over objectui's
59+
# blocked inventory found 7 blocks whose blocker had already closed — 58% of
60+
# that repo's machine-readable blocks were false, one of them for a week. The
61+
# same predicate had been catching objectstack's four every hour. The difference
62+
# was never discipline; it was that one repo had a caller.
63+
#
64+
# To adopt, in the sibling repo:
65+
#
66+
# 1. copy `scripts/pm/check-half-states.mjs` and this file, unchanged;
67+
# 2. open one `tracking`-labeled anchor issue there and set the repository
68+
# VARIABLE `HALF_STATE_ANCHOR_ISSUE` to its number
69+
# (Settings → Secrets and variables → Actions → Variables).
70+
#
71+
# That is the whole install. The swept repo needs no configuration at all: it is
72+
# `github.repository`, so the copy reads the board it lives in — a hardcoded
73+
# default was how a copied file could have swept THIS repo and written the
74+
# findings into a sibling's anchor, a fully green report about the wrong board.
75+
#
76+
# ⛔ Each install uses its OWN `secrets.GITHUB_TOKEN` and reads its own repo. No
77+
# cross-repo credential, no matrix over repos, no PAT: that route was refused at
78+
# grading (it buys no coverage a per-repo install lacks and raises the
79+
# credential floor for every repo at once). The accepted consequence is that a
80+
# cross-repo `Blocked-by:` target stays UNJUDGED in each install — H19 says so
81+
# in its own row rather than reading it as a healthy block.
5382

5483
on:
5584
schedule:
@@ -94,17 +123,34 @@ concurrency:
94123
cancel-in-progress: false
95124

96125
env:
97-
# The pinned anchor issue whose body this workflow owns.
126+
# The pinned anchor issue whose body this workflow owns — the ONE per-repo
127+
# input this file takes (#11217).
98128
#
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.
129+
# Resolution: the repository variable `HALF_STATE_ANCHOR_ISSUE` if set, else
130+
# this repo's own pinned number, else EMPTY — and empty makes the job refuse
131+
# to write rather than guess (see the "Resolve the anchor" step). The literal
132+
# is guarded by the repository name on purpose: an unguarded fallback is what
133+
# would let a verbatim copy in objectui rewrite ITS #9857 — some unrelated
134+
# card — with this board's findings, silently and four times a day. A number
135+
# is only ever meaningful in the repo it was minted in.
136+
#
137+
# TO ROTATE (here): open a new `tracking`-labeled issue, put its number below,
138+
# and note the handover in the OLD issue's body before closing it (its edit
139+
# history is the archive and does not travel).
140+
# TO ADOPT (a sibling repo): change NOTHING here — set the repository variable.
103141
#
104142
# The anchor deliberately carries `tracking` and NO `domain:*` label: `tracking`
105143
# is in the sweeper's own H13_EXEMPT_LABELS, so the anchor can never appear as a
106144
# finding in the sweep it hosts.
107-
ANCHOR_ISSUE: '9857'
145+
#
146+
# ⚠️ Folded scalar, and every continuation line sits at the SAME indent on
147+
# purpose: a more-indented line in a `>-` block keeps its newline literally
148+
# (measured on this very value), which would hand the expression parser a
149+
# multi-line string instead of one expression.
150+
ANCHOR_ISSUE: >-
151+
${{ vars.HALF_STATE_ANCHOR_ISSUE
152+
|| (github.repository == 'objectstack-ai/objectstack' && '9857')
153+
|| '' }}
108154
109155
jobs:
110156
patrol:
@@ -127,6 +173,13 @@ jobs:
127173
id: sweep
128174
env:
129175
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
176+
# WHICH board this run reads: the repo this workflow is installed in,
177+
# always. The sweeper would resolve the same answer on its own from
178+
# the runner's `GITHUB_REPOSITORY` (`resolveSweepRepo`), and it is
179+
# passed explicitly anyway so the wiring is visible to a reader of the
180+
# workflow — the two agree by construction and a copy of this file
181+
# cannot end up sweeping the repo it was copied FROM.
182+
PM_SWEEP_REPO: ${{ github.repository }}
130183
PROVENANCE: >-
131184
run [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
132185
· commit `${{ github.sha }}` · trigger `${{ github.event_name }}`
@@ -146,6 +199,29 @@ jobs:
146199
echo "check-half-states exited $code"
147200
cat "$RUNNER_TEMP/report.err" >&2 || true
148201
202+
- name: Resolve the anchor issue
203+
# An install with no anchor configured has nowhere to land its report,
204+
# and the ONLY safe behaviour is to say so loudly (#11217). The two
205+
# alternatives are both the failure this file exists to prevent:
206+
# guessing a number would rewrite an unrelated card in this repo, and
207+
# skipping the write quietly would leave a patrol that runs, finds, and
208+
# tells nobody — indistinguishable from a clean board.
209+
#
210+
# Placed AFTER the sweep so the run summary still carries the rendered
211+
# findings (the same "land the truth, then raise the alarm" order the
212+
# final step keeps), and skipped on a pull_request run, which never
213+
# writes an anchor at all.
214+
if: github.event_name != 'pull_request'
215+
run: |
216+
if [ -z "${ANCHOR_ISSUE//[[:space:]]/}" ]; then
217+
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)."
218+
exit 1
219+
fi
220+
case "$ANCHOR_ISSUE" in
221+
*[!0-9]*|'') echo "::error::HALF_STATE_ANCHOR_ISSUE is '$ANCHOR_ISSUE', which is not an issue number."; exit 1 ;;
222+
esac
223+
echo "anchor: #$ANCHOR_ISSUE in ${{ github.repository }}"
224+
149225
- name: Update the pinned anchor issue
150226
# A pull_request run proves the sweep; it must not touch the board.
151227
if: github.event_name != 'pull_request'

0 commit comments

Comments
 (0)