Skip to content

Widen the half-state patrol to the states the board actually produces: H24/H25/H26, sibling-repo adoption, and re-derived windows - #11294

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-11196-half-state-patrol-family
Aug 23, 2026
Merged

Widen the half-state patrol to the states the board actually produces: H24/H25/H26, sibling-repo adoption, and re-derived windows#11294
os-zhuang merged 4 commits into
mainfrom
claude/issue-11196-half-state-patrol-family

Conversation

@claude

@claudeclaudeBot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes#11196
Fixes#11217
Fixes#11219
Fixes#11118

One family, one defect shape: the patrol's input set and predicate vocabulary were narrower than the states the board actually produces. Four graded cards, one commit each, all on scripts/pm/check-half-states.mjs and its workflow — the hot-file family dispatched as a fold rather than serialized.

commitcardwhat lands
6bc3e47#11196H24 (pm:queue + assignee), the pm:awaiting-maintainer state, the paired-write remedy texts
5d86dc6#11217the sweeper/workflow pair parameterised for verbatim sibling-repo adoption
1604623#11219H26 — a block whose target can never close, plus the transitive-wait flag
02e2838#11118H8/H22/H23 window arithmetic re-derived against the measured merge rate

Report-only throughout: no predicate here is a gate, no row writes a label, and a completed sweep still exits 0 with 0 or 40 findings.

#11196 — the two-views contradiction, and the state that had nowhere to live

H24 fires on an open card carrying pm:queue with a non-empty assignee. The queue view reads it as dispatchable, the claim rule reads it as taken; both readers are right about the field they read, so the card is available to everyone and forbidden to everyone at once. Every adjacent row declined the shape — H1 wants no assignee, H2 wants a missing claim comment (the measured carriers had complete ones), H3 wants two labels while here the second half of the contradiction is a field — which is how 17 cards across three repos sat in it unreported. Those three adjacencies are pinned as self-test cases, not asserted.

The predicate is a pure intersection: no threshold, no timestamp, no identity test. Per the ruling of 2026-08-23 the rule lands first and any true-ownership exemption is an explicit marker later — so the row fires on a human assignment too, names the login so residue and ownership are separable by a reader, and states the asymmetric remedy (an agent may clear agent residue; ⛔ never a human's).

Paired writes (fix 2). The landing re-label (H8) and the unlock scan (H19) are the two rollback paths that never named the assignee drop; both remedy sentences now carry 「同笔摘 assignee」. H8's half-delivered branch deliberately does not — there the label and the claim are correct, and prescribing a drop would be the destructive write #10468 closed.

pm:awaiting-maintainer (fix 5) is implemented as a first-class state, spelling finalized as ruled:

  • vocabulary entry in scripts/pm/ensure-pm-labels.sh (five-repo loop, 88-char description, consumers named in its comment block);
  • H25 pins its mutual exclusions — pm:queue, pm:dispatched, pm:blocked, pm:on-hold, needs-user-decision — each with the specific lie it tells rather than a generic "these disagree";
  • membership in H11's parked inventory (with the exit sentence this state actually has: no machine exit, by construction), H13's state vocabulary (or every carrier would become a standing H13 row two hours in) and H22's residue set;
  • one label page in the sweep's listing loop, so a card carrying only this state is swept at all.

Deferred, declared not dropped: the SKILL.md state-model table row for the new label (protocol face, fable-mandatory, separate landing), and applying the label to specimen card #7276 — the seat's write at ACCEPT, never this PR's. The legacy cleanup ruled on the same day was already executed and is untouched here.

#11217 — the pair a sibling repo can adopt verbatim

Two hardcodings made "copy both files" unsafe, and both are gone:

  • the swept repo resolves PM_SWEEP_REPOGITHUB_REPOSITORY → default. A copy of the script in objectui, run with no override, used to sweep objectstack and write those findings into objectui's anchor: a fully green report about the wrong board. A malformed value is refused at the CLI (exit 2) instead of silently falling back — substituting a different board is the disease itself.
  • the anchor issue comes from the repository variable HALF_STATE_ANCHOR_ISSUE, with 9857 kept only as a fallback guarded by the repository name. An install with no anchor configured fails loudly after the sweep (so the run summary still carries the findings) rather than rewriting whatever #9857 happens to be in that repo.

Per-repo installs, each with its own GITHUB_TOKEN, per the grading ruling — no matrix, no cross-repo credential. The accepted consequence (cross-repo Blocked-by: targets stay UNJUDGED per install) is stated in both headers; #11218's loudness half is not addressed here and stays open.

Objectstack leg unchanged — measured, not asserted. The old value was the literal 'objectstack-ai/objectstack'; the runner sets GITHUB_REPOSITORY (and now PM_SWEEP_REPO) to that same string, so every request path is byte-identical. Observed offline by stubbing globalThis.fetch and running the real CLI:

GITHUB_REPOSITORY=objectstack-ai/objectstack -> GET https://api.github.com/repos/objectstack-ai/objectstack
GITHUB_REPOSITORY=objectstack-ai/objectui -> GET https://api.github.com/repos/objectstack-ai/objectui
PM_SWEEP_REPO=…/cloud (override wins) -> GET https://api.github.com/repos/objectstack-ai/cloud
PM_SWEEP_REPO=objectui (malformed) -> refused, exit 2, no request made

Plus 20 self-test cases over resolveSweepRepo, including the objectstack runner's exact env shape and the empty-string case an unset Actions variable expands to.

⚠️ One YAML trap worth naming: in a >- folded scalar a more-indented continuation line keeps its newline literally, which would hand the expression parser a multi-line string. Measured on this very value and fixed by aligning the continuations; the comment above it says so.

#11219 — the block nothing can ever release

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. Such a block is structurally indefinite, and the waiting card passes every existing check: line present (H4), target resolves and is open (H19), label correct. H9 — the nearest neighbour — audits the held card, not the waiting one. All six measured instances were found by a human reading; one repo's 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 (a request per hop, and chains can cycle).

Quota-neutral, as the card asked: 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. Not reported, deliberately: a target labelled pm:queue while titled [Decision] (one of the six) — a mislabelling, not a fact in the labels, and a title heuristic would make the sweeper guess at intent.

#11118 — window arithmetic re-derived, with the windows pinned as ISO instants

H8's docblock justified two pages with ~18 merges/day. Re-measured 2026-08-23T08:42:15Z against the live endpoints, windows pinned as full ISO instants per the card's approxidate warning:

windowreadingspanverdict
merge rate300 commits, 2026-08-21T04:00:19Z … 2026-08-23T08:22:47Z2.18 d~137.5/day (300/300 carry the squash marker)
H8, 2 pages200 closed PRs, 197 merged, oldest 2026-08-21T14:00:28Z1.78 d~7× tighter than its prose claimed
H8, 4 pages400 rows, 397 merged, oldest 2026-08-20T09:41:02Z2.96 dwidened
H22, 2 pages200 closed issues, updated 2026-08-22T17:05:44Z … 08-23T08:39:14Z0.65 d (~15.6 h)~2.6 sweeps — thin
H22, 4 pages400 rows, updated from 2026-08-21T15:44:49Z1.70 dwidened
H23, 3 pages300 commits, 2026-08-21T04:00:19Z … 08-23T08:22:47Z2.18 dunchanged, ~8.7 sweeps

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 exists 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 produced fastest. Four pages restores what its own anti-drowning argument was choosing (a couple of days) without reopening 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 executablewindowCoverageDays, 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 core quota.

Verification

Self-test: 879 → 997 cases pass, both directions pinned for every new predicate, every vocabulary membership, and each window constant (including the degenerate inputs that must answer "cannot divide" rather than Infinity). One pre-existing pin — H22's "the measured residue set is the five from the census" — failed on the new label and was updated deliberately, with the reason for the one non-census member written into it.

Live sweep not run, and it cannot be here: this container's GITHUB_TOKEN is the proxy's 14-character placeholder and the anonymous quota is spent, so --probe returns the classified PREREQUISITE NOT MET (exit 3) the file's own header documents. Self-test plus the offline fetch trace above is the verification surface; the workflow's pull_request trigger runs the real sweep on this PR against a runner where the transport prerequisite is met.

Gates derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack on the actual change set at 02e28386 (three-dot, merge-base 050d8d868) — 20 families named, each run with its exit code captured before any pipe:

cross-package-test-inputs EXIT=0 | OK: 14 package(s) read outside themselves, all declared…
entry-guard EXIT=0 | ✓ check:entry-guard: 138 scripts/ file(s) — every entry guard goes through invoked-as.mjs
node-version EXIT=0 | Node 22 is in maintenance; supported until 2027-04-30
parse-guard EXIT=0
pm-half-states EXIT=0 | ✓ check-half-states self-test: 997 cases pass.
pm-label-desc-cap EXIT=0 | ✓ 21 label descriptions … all ≤100 characters
pnpm-filter-targets EXIT=0 | ✓ 120/148 `--filter` occurrence(s) … resolve
required-contexts EXIT=0
shard-attestation EXIT=0 | ✓ 2 aggregate gate(s) count 3 declared leg(s)
workflow-status-functions EXIT=0 | OK (26 workflow file(s), 49 job(s) …)
aggregator-roster EXIT=0
ci-filter-parity EXIT=0 | OK: all 88 declared cross-package glob(s) … covered
closing-keyword-parity EXIT=0 | OK (3 parsers agree on all 9 keywords …)
step-collectors EXIT=0 | ✓ 358 `run:` steps across 26 workflow(s)
nul-bytes EXIT=0 | OK (scanned 6335 text file(s) … no raw ASCII control bytes)
partof-closing-keyword EXIT=0 | run with this PR body (see below)

pnpm lint (repo-wide eslint . --no-inline-config, not a narrowed subset): exit 0 in 78 s, no output. check:partof-closing-keyword needs a PR body to judge and was run against this one.

skip-changeset: nothing here is published — scripts/pm/** tooling and one workflow. No user-visible surface changes, so no changeset is owed.


Generated by Claude Code

… 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
…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
…e 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
…ured 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
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 23, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 23, 2026 09:14
@os-zhuang
os-zhuang added this pull request to the merge queueAug 23, 2026
Merged via the queue into main with commit 5e3caa1Aug 23, 2026
33 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-11196-half-state-patrol-family branch August 23, 2026 09:31
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment