Skip to content

pm(patrol): seat-idle and blind-claim predicates, and make UNJUDGED survive the body trim - #12088

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-11706-patrol-predicates
Aug 25, 2026
Merged

pm(patrol): seat-idle and blind-claim predicates, and make UNJUDGED survive the body trim#12088
hotlong merged 1 commit into
mainfrom
claude/issue-11706-patrol-predicates

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes#11706, closes#11724, closes#11218.

Three patrol-predicate cards folded into one PR because they share one hot file
and one gate family: scripts/pm/check-half-states.mjs and its --self-test.
Non-governed surface. Report-only is preserved end to end — a completed
sweep still exits 0, and no row here writes a label, a title or a body.

Self-test: 1193 → 1336 cases, all passing.


Per-item checklist

#11706 — H32, a HELD seat idle over a non-empty lane queue

The first predicate in this file that watches the seat rather than a card.
The patrol could see a card nobody moved (H30) and could not see a lane whose
seat had stopped moving it.

  • Threshold derived from data, the H30 way — ⛔ not guessed. Census of
    every Claim: comment on the six active lanes' open pm:dispatched cards
    plus the recently-closed window, n = 169 inter-claim gaps:
    p50 7 min · p75 41 · p90 324 · p95 731 · max 6073.
    The distribution is bimodal — inside a wave a seat claims a batch
    minutes apart, between waves it goes quiet for hours — and reading it as
    one hump is the trap. Inspected individually, all 12 gaps above 480 min
    span a shift boundary or a night
    (domain:devx 08-19T11:48 → 08-23T17:01;
    domain:engine 08-24T15:22 → 08-25T01:47 across a 收班/开轮 pair).
    Nothing measured within a held shift reached 480. SEAT_IDLE_STALE_MINUTES = 480 therefore sits above the whole active-seat tail and below the
    cross-shift band. The derivation is written into the predicate's header.
  • Legitimate waits excluded structurally, not by threshold — a latest
    marker naming 等 CI / 等裁决 / 等人工步骤 or an awaiting-class state exempts
    the seat regardless of elapsed time.
  • ⛔ No new prose rule anywhere. The card's own point is that the rule is
    not missing (「规则不缺,措辞也不含糊」); the row says so in as many words so
    it cannot be re-read as one.
  • Structural gates in front of the clock: held (🟢 with a real holder) ·
    lane countable on this board · unclaimed queue > 0 · in-flight = 0.

Reverse verification, on real seat posts with real marker prose — only the
lane inventory was forced to the idle shape:

seatlanewait declared in its real marker?verdict
#6017domain:specnoFIRES (latest marker: Round-start marker — R6.)
#6024domain:clinoFIRES
#6023domain:devxnoFIRES
#6367domain:engineyesclean (exempt)
#6021domain:servicesyesclean (exempt)

Against the live board it is correctly silent on all 12 seat posts: every
objectstack lane currently has work in flight.

#11724 — H33, a claim written before the ruling that now stands

#11218 — H19 cross-repo, scoped per the 2026-08-25 premise refresh

Half 1 (cross-repo read) — reported, not taken. A genuine cross-repo read
needs a credential the patrol does not hold and, by standing ruling, will not be
given: .github/workflows/half-state-patrol.yml states it (「⛔ Each install uses
its OWN secrets.GITHUB_TOKEN and reads its own repo. No cross-repo credential,
no matrix over repos, no PAT」 — refused at grading, per-repo install chosen), and
an Actions token is repo-scoped by construction. ⛔ Token-scope widening is
routing/security's call and is not taken here.

What did land is the honest half, and it needs no credential change:

  • The cause is now MEASURED per target, never inferred. A target 404 is
    ambiguous — unreachable repo, or a number that is not there. One
    GET /repos/<owner>/<name> per distinct sibling repo (cached, bought
    lazily only when a cross-repo target actually failed) separates them, the
    same two-stage shape classifyRepoRead already uses. Repo unreadable ⇒ the
    row says so per target and names it a standing accepted limit no re-run
    will fix; repo readable ⇒ the number is not there; probe itself failed ⇒
    neither side is claimed.
  • Verified what the per-target row renders, and topped it up (the
    premise-refresh asked for exactly this). It now states that UNJUDGED is
    not a quiet row and that the block is as unverified as if nothing had
    been read
    .

⚠️ And a measured falsehood the verify turned up. The summary sentence
claimed 「each unresolved target is named on its own card's row, never
dropped
」. On the 2026-08-25T02:08Z sweep that was false: the run resolved
25 of 28 targets and rendered five H19 rows, all same-repo and closed-led, not
one carrying an unresolved target
— because 199 rows were trimmed to fit the
body limit. 199 further row(s) omitted and every unresolved target is named
sat in one body contradicting each other. That is #4690 wearing this item's own
uniform.

  • Fixed by ordering, not budget: unjudged rows now sort into their own
    band above ordinary rows, so the trim can only ever fall on rows that made
    a determination. Same reservation renderTriggerIndex already gets.
  • The summary now names the mechanism instead of promising an outcome,
    and a banner announces unjudged rows and says a later sweep will not
    resolve them.

Note on evidence: the probe's disambiguation was exercised against real repos
from this container, whose credential can read the siblings
(/repos/objectstack-ai/cloud → 200, issues/944 → 200, issues/99999999
404). The patrol runner's repo-scoped token cannot, so on the runner the same
code takes the repoReadable === false branch. That branch is pinned by the
self-test; it is not claimed here as measured on the runner.

#11604 appendix verify-item — covered, no new row (card left open)

The shape (pm:queue + assignee + merged delivering PR + no remote branch) is
already covered by H24 (h24QueuedWithAssignee), which fires on the pure
pm:queue ∩ non-empty-assignee intersection with no threshold, no timestamp and
no identity test. The extra conditions in the appendix only narrow that set, so
every card of that shape is already reported. No predicate added; the
appendix item is the row H24 was filed for (17 carriers across three repos at the
2026-08-23 census). #11604 is not addressed by this PR beyond this
verification and remains open for its other items.


Bounded in-place fix (declared)

While editing the counts object for the new coverage pairs I found a wiring
regression in it: dispatchRefTargets / dispatchRefRead were computed by the
H20/H27 pass and never forwarded
, so the dispatch-liveness clause rendered
read on 0 of 0 on every live sweep since it was added. Evidence: the
2026-08-25T02:08Z run reports remote branch read on 0 of 0 distinct claimed branch(es) while publishing two H20 findings, which only a non-empty ref
cache can produce.

Same defect class as this PR's #11218 work (a coverage pair going silent —
#4690), mechanical, the correct shape pinned by its ten sibling keys, same file,
same gate family, no new verification surface. Rather than adding two lines, the
forwarding is now an enumerated contract (SWEEP_COUNT_KEYS) that sweep()
copies, so forgetting a key is no longer something the assembly can do — and the
self-test asserts the contract directly.

Not in scope — filed separately

CLAIM_COMMENT_MARKER requires a colon, and 24 of the 54 open
pm:dispatched cards carry a well-formed claim written with an em dash
(Claim — <seat>, session …), which the marker cannot see. H2 consequently
reports those 24 as carrying no claim comment at all, and H20/H27 are blind to
them. H33 inherits the blindness deliberately — a file that disagrees with
itself about what a claim is would be worse than the gap, and the direction is
under-reporting, never a fabricated row. Documented in the header and pinned by
two self-test cases; filed as its own card.

Verification

Run against final commit b35f0f6e8:

  • node scripts/pm/check-half-states.mjs --self-test✓ check-half-states self-test: 1336 cases pass. (baseline on origin/main was 1193)
  • Gate family derived mechanically with
    node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    (12 families), all green, exit codes captured before any pipe:
    check:pm-half-states · check:nul-bytes · check:agent-test-spelling ·
    check:cross-package-test-inputs · check:entry-guard · check:parse-guard ·
    check:partof-closing-keyword · check:pnpm-filter-targets ·
    check-ci-filter-parity · check-closing-keyword-parity ·
    check-cross-package-test-inputs · check-partof-closing-keyword
  • check:nul-bytesOK (scanned 6704 text file(s) … no raw ASCII control bytes), plus a direct control-byte scan of the changed file
  • Repo-wideeslint . --no-inline-config5090 files, 0 errors, 0
    warnings
    (the full population, not a narrowing)

No changeset: scripts-only, nothing user-visible is published (skip-changeset).


Generated by Claude Code

…JUDGED survive the trim
Three cards, one hot file, one surface (`scripts/pm/check-half-states.mjs`
plus its `--self-test`). Report-only throughout: a completed sweep still
exits 0 and no row writes a label.
H32 — a HELD seat idle over a non-empty lane queue. The first predicate here
that watches the SEAT rather than a card: the patrol could see a card nobody
moved and could not see a lane whose seat had stopped moving it. Threshold
derived the H30 way from the measured inter-claim distribution (n=169 over six
active lanes: p50 7 min, p90 324, p95 731); 480 min sits above every measured
within-shift interval, and all 12 gaps past it span a shift boundary. Legitimate
waits are excluded structurally, not by clock — a marker naming a live blocker
exempts regardless of elapsed time.
H33 — a `pm:dispatched` card whose latest `Claim:` predates a triage ruling on
the same thread, i.e. work in flight under an order that cannot carry the ruling
now standing. Ruling anchors are a closed, measured set. Low-yield by
construction: 0 on today's board, 4 of 107 over the closed window.
H19 — the cross-repo half that can actually land. A genuine cross-repo READ
needs a credential the patrol does not hold and by standing ruling will not be
given, so the work is making UNJUDGED honest instead: one `GET /repos/<o>/<n>`
per distinct sibling repo turns an ambiguous target 404 into a measured cause,
per target. And the summary's "never dropped" promise about unresolved targets
was measurably FALSE — the 2026-08-25T02:08Z sweep trimmed 199 rows and rendered
no unresolved target at all — so unjudged rows now sort above the size trim.
Also fixes a wiring regression found while editing the same object:
`dispatchRefTargets`/`dispatchRefRead` were computed and never forwarded into
`counts`, so the dispatch-liveness clause rendered `read on 0 of 0` on every
live sweep. The forwarding is now an enumerated contract the self-test pins.
Self-test 1193 -> 1336 cases, all passing.
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 25, 2026
@hotlong
hotlong marked this pull request as ready for review August 25, 2026 09:16
@hotlong
hotlong added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit a33b4d3Aug 25, 2026
33 checks passed
@hotlong
hotlong deleted the claude/issue-11706-patrol-predicates branch August 25, 2026 09:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment