Skip to content

pm(half-states): H66 lists a released pm:queue card whose remainder is not work - #18514

Merged
os-justin merged 5 commits into
mainfrom
claude/issue-18299-released-nondispatchable-remainder-row
Sep 16, 2026
Merged

os-justin merged 5 commits into
mainfrom
claude/issue-18299-released-nondispatchable-remainder-row

Conversation

@os-justin

@os-justin os-justin commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #18299

A card released with a remainder no dev can execute — a maintainer ruling, a reading only the reporter can take, a confirmation owed by another seat — wears pm:queue with no assignee, which the queue view and 「最老优先」 both read as ready to hand to a dev. The label cannot tell that apart from real queue work; only the card's newest seat comment can, and reading that costs a full card read per candidate.

H66 is the detector the card asks for: report-only, a listing, never a relabel and never a gate verdict.

Authored by session_01Gqi43smmqjJ5sUrhfoPeKu (Claude Code). Every reading below was taken on this branch at 301de97, the head this body describes.

The clause it enforces

Two lines of .claude/skills/pm-dispatch/SKILL.md, quoted verbatim in the row's own sentence:

  • 「pm:queue = 有具名落点或复现的具体缺陷,或范围明确的工具/门禁修复,无可问之事。」 — the label is a definition, not a habit.
  • 「释放是显式动作:让卡离手者同笔清 assignee + Release: 行(会话/因/去向);下一任重新认领。」 — the release act, whose 去向 field is the only thing that can say where the card went.

⛔ The row does not decide the state question. Whether these cards belong in needs-user-decision, in pm:awaiting-maintainer or in a spelling that does not exist yet is left open exactly as the card leaves it; the remedy the row prints is 「judge by hand — this is a listing」.

The measurement — the dispatch's A1 was falsified before anything was built

A1 assumed the machine-readable half is the Release: line's third field (去向). Measured 2026-09-16 over both boards the filing card names, read-only:

corpus threads Release: lines release announced in a heading
objectui#7696 · #8167 · #8826 · #8938 (the filer's four) 4 0 3
objectstack, 25 most recently updated open unassigned pm:queue cards 25 0 0

Zero of 29. The lit control for that zero is the prose leg read by the same fetch over the same threads: it answers three on the same four specimens, so the zero is a property of the board, not of a broken reader.

⇒ Both legs ship, the canonical one first. It is kept, not dropped, for a mechanical reason: H47 exists to report exactly that absence, and as it is answered the canonical line starts appearing — on that day it is the only reading that can tell a return-to-queue (去向 = pm:queue, ⛔ never a finding) from an exit, because both are written in the same announcement heading otherwise. The summary clause publishes the zero on every run.

The prose leg's three anchors are frozen beside the live comment each was measured on — objectui#7696 5664140343, #8167 5663517223, #8938 5668851500 — and the third anchor named by the filing card itself is labelled measured: false, so nobody later reads it as a measurement. Matching is heading-anchored (H58's fence/blockquote/table/code-span refusals, reused): the filing card, this PR and the row's own docblock all quote the spellings, and none of them may fire.

⚠️ pm:queue is not read over the whole comment body, and that is measured rather than stylistic: the two live release comments on objectui#8167 and #8938 both say 「pm:dispatched stripped, pm:queue added」 in their state-write paragraph, so a body-wide read would clear precisely the specimens this row exists to list.

The bounded fetch leg — and why the first landing needed one

The row first shipped buying nothing: one commentCache lookup per card. The first live sweep measured what that costs — 194 candidates, a thread in hand for 1. The corpus is thin by construction, not by luck: the only row that buys a card thread for a pm-tracked card is H2's claim read, and H2 buys one only for an ASSIGNED card, which is this population's exact complement. A listing that cannot see 99.5% of its own population is a row whose silence means nothing, and the filing card's own criterion already asks for the fetch — 「fetch the newest comment authored by a seat」.

So a bounded buy pass now sits at the foot of the sweep:

  • One page per card, the NEWEST one, located from the card's own comments count through H65's exported h65NewestPagePlan / h65CommentPagePath rather than a second copy of the arithmetic. ⛔ Never page 1 on a long thread: GitHub serves issue comments OLDEST-FIRST, so a page-less read returns the archive (the [finding] check-half-states: H44's seat leg is blind on the triage seat post it was written for — its corpus is the thread's FIRST comment page (oldest-first, five weeks stale on #6015) and H44_ROUND_OPEN_MARKER no longer matches the marker spelling; H56 / H64 share the corpus #18312 measurement — five weeks stale on a long thread).
  • Ordered newest-touched first (updated_at descending, number as a total-order tiebreak; an unreadable stamp sorts last, never promoted ahead of a dated card). A release is a write on the card, so that is where fresh evidence lives — deliberately the opposite of the dispatch order 「最老优先」, which says which card to work, not where the evidence is.
  • Capped by a named, frozen H66_THREAD_BUY_CAP = 100. The standing caller is half-state-patrol.yml on PATROL_CRON = 37 1,7,13,19 * * * — four runs a day — under the Actions token's 1000 requests/hour per repository, beside the sweep's existing label pages, listings, PR windows, H50 walks, H43 probes, H45 parent reads and seat pages.
  • A cap is not a coverage claim. Candidates past it are NOT ATTEMPTED and counted separately (H40's posture). A fetch that fails, or a carrier whose comments count is unreadable and whose page 1 came back FULL (so the newest comment cannot be located), leaves that card UNJUDGED — never clean, the H4 three-state contract — and is counted as such.
  • Placement is mechanical: after every row that reads commentCache, because a newest page is not the same object as the first page that cache holds. The write-back is guarded to the one case where the bought page is the whole thread (plan.page === 1); seatPostRowsFor keeps its own window out of that map for exactly this reason, and this applies the same rule to the one case where the two coincide. A card whose thread is already in hand is judged in place, free, and never deferred — so never re-bought.

seatWindowAudit gains seatPurchaseSites. The buy adds a second located-page purchase, so rather than raise the file-wide pin from 1 to 2 and lose the argument behind it, the seat-post bound is split out and stays at 1 — the new purchase is on a disjoint population (an unassigned pm:queue CARD, never a pm:seat post), so it cannot be the 「same page bought twice」 the original bound is about.

Disjointness (the dispatch's A3), and the one overlap stated rather than hidden

  • H8 requires pm:dispatched; this population is pm:queue without it. Disjoint by label.
  • H47 leg (a) requires an assignee; leg (b) is the inverse of this row — it fires when a Claim: is the newest ownership record and no Release: answers it. On the canonical leg releaseAnswersClaim is true and H47 returns null.
  • H49 requires pm:dispatched and an assignee. Disjoint.
  • H58 reads the card's own declaration of what it IS; this reads the release record of where it WENT. A card may wear both and fire both — two readings, never one restated.
  • ⚠️ The overlap: on the prose leg the release was never written as a canonical line, so H47 leg (b) fires too. That is correct and is a different defect (a record missing its canonical line) from this one (a card in the dispatch pool with a remainder). Asserted as a case, not hidden.

Population excludes an assigned card (H24's row) and any card carrying a second member of PM_EXCLUSIVE_STATE_LABELS — that pair is H3's, H25's or H29's contradiction. One vocabulary, ⛔ not a second list.

Battery roster and floor

This file printed one total over a flat case list — the shape AGENTS.md 「Writing a --self-test」 names as evidence rather than proof. The handshake (SELF_TEST_VERDICT) was already here; the floor was not. This PR adds the other half, copied from scripts/check-agent-model-declared.mjs and ⛔ never imported:

battery registered at 301de97 pin
H66 released queue card 183 172

Three failure conditions, each producing a real process.exit(1): a declared battery below its pin; a case registered against an undeclared battery; the roster below SELF_TEST_BATTERY_FLOOR. ⚠️ The roster is partial and says so: the several hundred pre-existing t(...) cases stay unbatteried, and retrofitting them is a diff of its own.

Cases cover every clause the dispatch named — (a) a 去向 naming a non-queue owner ⇒ listed · (b) 去向 = pm:queue ⇒ silent, including where an announcement heading sits beside it · (c) both prose forms ⇒ listed · (d) any later comment ⇒ silent, structurally, because the row reads the newest · (e) an assigned card ⇒ outside the population · (f) no comment at all ⇒ silent — plus the seven quoting shapes that must not fire, the anchor set's cap/freeze/provenance, the three-input-state contract, band, registry coverage, and the summary clause in both media. The buy leg adds: the cap respected when candidates exceed it, newest-touched ordering with the undated card last, a bought page judged by the same predicate as a cached one, a failed buy read as UNJUDGED, and the two source-audited guarantees below.

Measurements — all at 301de97

Gates — node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 40 commands at 301de97; all 40 run, each exit code captured by redirect-then-$? and never through a pipe, 40 exit 0. Reconciled with --ran: 「40 derived famil(ies) accounted for — 40 run, 0 NOT-MEASURED (a DERIVED zero — all 40 recorded an exit code and none of them is 3)」.

Lint — the whole-repo run, not a narrowing: pnpm lint = eslint . --no-inline-config over eslint's own population of 6,798 files, exit 0 at 301de97.

Self-test — node scripts/pm/check-half-states.mjs --self-test and pnpm check:pm-half-states at 301de97: exit 0, 「4546 cases pass. Batteries: H66 released queue card 183/172.」

Live sweep — report-only, from an agent container at 301de97, exit 0: 465 half-states over 397 pm-labelled issues. The H66 clause, the new quadruple:

Released queue cards (H66): 197 open unassigned pm:queue card(s) could be spoken about, 1 with a comment thread ALREADY in hand, 100 NEWEST comment page(s) BOUGHT this run (cap 100, 96 candidate(s) NOT ATTEMPTED at that cap), 0 listed. 0 card(s) stayed UNJUDGED.

⇒ the leg works and costs what it declares: 100 pages bought, every one read, nothing unjudged. The board genuinely has no card in this shape right now — the filing card's own four specimens were all dispositioned by hand between filing and this build. Before the leg the same sweep read 194 / 1 / — / 0, i.e. the row could see one card in two hundred.

Request count — the run total is NOT MEASURED: the script exposes no request counter, and the container's /rate_limit proxy reported a full 15000/15000 both before and after with the quota window rolling between the two reads, so the delta is not a measurement either. What is exposed is the leg's own cost, and it is exact: queueReleaseBought = 100, one request each, printed on the summary line every run.

Ablations — three, each mutating the committed tree, proving the mutation on disk by occurrence count, restoring with git checkout HEAD -- path under a trap, and verifying the restore by blob hash against git rev-parse HEAD:path plus an empty git diff HEAD:

  1. the release-announcement anchor rewritten so it can never match ⇒ 29 named cases red, suite runs to completion, exit 1.
  2. the battery pin raised above the registered count ⇒ 「the battery floor failed — battery 「H66 released queue card」 registered 183 case(s), below its pin of 200」, exit 1.
  3. the defer guard removed, so a thread already in hand would be re-bought ⇒ exactly one named case red — 「⭐ …and that place is guarded by the cache test, so a thread ALREADY in hand is never re-bought (got 0, want 1)」 — exit 1.

Control after every restore: exit 0, 4546 cases pass. ⭐ The first ablation also found two real defects in the battery it was measuring — a direct [0].heading and a direct field read on a three-valued helper, each of which aborted the whole suite with a TypeError instead of reporting a case. Both are now read through describing readers, with the measurement recorded beside them.

Renderers — both, offline over a synthetic finding: renderPlain and renderMarkdown each render the H66 row, the clause with its counts, and the markdown family ledger names H66.

Scope

scripts/pm/check-half-states.mjs only. ⛔ objectui's copy is byte-pinned there and is not touched. skip-changeset: scripts/pm/** is the fast lane and nothing in any package's files[] moves.


Generated by Claude Code

… is not work

A card released with a remainder no dev can execute wears `pm:queue` with no
assignee, which the selection order reads as ready to dispatch. H66 reads the
card's NEWEST comment out of the shared cache -- no new fetch class -- and
lists the card when that comment is a release the label contradicts.

The canonical `Release:` line's 去向 was measured first and came back ZERO of
29 threads on two boards, with the prose leg lit on 3 of the same 4 specimens
as the control; both legs ship, the canonical one first, and the summary clause
publishes the zero. Report-only: it never relabels and never returns a verdict.

Adds this file's first battery roster + floor beside the existing handshake.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <noreply@anthropic.com>
…t `[0].heading`

Measured during the anchor ablation: the direct index throws while evaluating
the case's arguments, so the suite ABORTS at that line and every later case
never runs -- the exact shape this file's own row-wrapper note warns about.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <noreply@anthropic.com>
Second abort site measured by the same ablation: `h66ReleaseVerdict` is
three-valued, so a direct field read throws in the case's arguments and takes
every later case with it. Both readers keep a nulled helper reporting under its
own case name.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <noreply@anthropic.com>
The first live sweep measured the cost of buying nothing: 194 candidates, a
thread in hand for ONE, because the only row that buys a card thread buys it
for an ASSIGNED card and this population is that set's complement. The row now
defers cache misses to a bounded pass at the foot of the sweep: candidates
ordered newest-touched first, at most H66_THREAD_BUY_CAP=100 per run, ONE page
each -- the NEWEST, located from the card's own comments count through H65's
helpers, never page 1 on a long thread.

A failed buy, or a carrier whose newest page cannot be located, is UNJUDGED and
counted as such; candidates past the cap are NOT ATTEMPTED and counted
separately. The cache write-back is guarded to the one case where the bought
page IS the whole thread, so no reader is handed a partial window.

seatWindowAudit gains seatPurchaseSites: the second located-page purchase is on
a disjoint population, so the fetch-per-seat-post bound is split out and kept at
one rather than raised.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl and removed size/l labels Sep 16, 2026
@os-justin
os-justin marked this pull request as ready for review September 16, 2026 18:43
@os-justin
os-justin added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit 9e6c6cf Sep 16, 2026
42 checks passed
@os-justin
os-justin deleted the claude/issue-18299-released-nondispatchable-remainder-row branch September 16, 2026 19:05
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…tstack-ai#18654)

Fixes objectstack-ai#18379

H19 gathered `Blocked-by:` targets over the **body ∪ comment** union and
judged the expiry against the **body's** set (objectstack-ai#17564), but the
`UNJUDGED` branch never moved with that split: it stayed founded on the
**resolution** set, so a target a seat had **superseded in the body**
could still veto the verdict from the comment archive whenever its
number was **unreadable**. Two conditions had to coincide, which is why
it outlived objectstack-ai#17564 — the target is superseded (judging correctly ignores
it) **and** the superseded number 404s (resolution cannot dismiss it
either) — and the resulting row was **unclearable by construction**: the
card's owner had already done the prescribed thing, and the only
remaining lever, editing the archived comments, is the write the
protocol forbids because those comments record what an earlier seat
measured and why the line was migrated.

_Authored by `session_01Gqi43smmqjJ5sUrhfoPeKu` (Claude Code). Every
reading below was taken on this branch at **`ab30706ed`**, the head this
body describes._

## The two sets, before and after

`blockerTargetsFor` is untouched: DISCOVERY stays a union, because H26
and H28 share those resolutions and a comment-borne target is a
perfectly good answer to what they ask. What changed is inside
`h19BlockOutlivedBlocker`.

| reading | may FOUND a row — before | may FOUND a row — after |
|:--|:--|:--|
| CLOSED, body-borne (or comment-borne on a body-less card) | yes | yes
(unchanged) |
| CLOSED, comment-only beside a body set | no — hygiene clause | no —
hygiene clause (unchanged) |
| UNRESOLVED, body-borne (or on a body-less card) | yes — `UNJUDGED` |
yes — `UNJUDGED` (unchanged) |
| **UNRESOLVED, comment-only beside a body set** | **yes — the
unclearable row** | **no — named in its own clause** |
| OPEN, either channel | no — withholds only | no — withholds only
(unchanged) |

The denominator moved with it: an archive-only unreadable target no
longer enters the row's `N of M` count, which was the other half of the
sentence the card quotes (「1 of 2 … the set judged against the BODY's
set」 on a card whose body names one target).

**What the archive is still consulted for, and must be:** evidence that
the wait may still be **RUNNING**. An OPEN target it names still
withholds a discharge, and an UNREADABLE one now withholds a **FULL**
discharge in a new branch (「No target this row JUDGED is still open, but
N target(s) the comment archive names … could not be read at all」).
Narrowing that half to the body too would re-create **objectstack-ai#11747** — the
measured mirror defect, where a re-park parks the live blocker in a
comment while the body still names the spent one, and a card was
released to `pm:queue` while its real blocker was open and dispatched.
Only the power to FOUND a verdict is carrier-scoped; evidence of a live
wait never is.

⛔ `UNJUDGED` is **not** silenced. Three controls say so live, below.

## Pins

Battery **`H19 judged-set founding`** — the file's second registered
battery (`SELF_TEST_BATTERIES`), **37 cases registered, pinned at 34**;
`SELF_TEST_BATTERY_FLOOR` raised `1` → `2`, so deleting either battery
now fails the roster rather than shrinking a total nobody reads.

It floors a NEGATIVE — a row that must not fire — plus the controls that
keep the silence scoped:

- the instance from the card, in its recorded shape (objectstack-ai#11975: body
refreshed to `Blocked-by: objectstack-ai#18336`, the archive naming `objectstack-ai#13515`, resolved
404) ⇒ discovery still yields both targets, and **no row of any kind**;
- the same shape generically, cross-repo and local ⇒ no row;
- CONTROL ①: a body-borne unreadable target ⇒ `UNJUDGED` fires, counted
`1 of 1`, still refusing to be read as a quiet row;
- CONTROL ②: a body-less card ⇒ still judged from comments, `UNJUDGED`
fires;
- CONTROL ③: a closed body target ⇒ H19's ordinary row, `1 of 1`, full
discharge;
- CONTROL ④: objectstack-ai#11747's mirror ⇒ an OPEN archive target still withholds,
still `1 of 2`, still `PARTIAL`;
- the withholding path: an archive-only unreadable target beside a
closed body target ⇒ the row fires on the body target, is ⛔ not
published as a full discharge, and names the archive target in a clause
saying it founds nothing;
- ⛔ H26/H28 unaffected: the union still discovers both channels.

**Two existing pins were re-judged rather than deleted**, and both are
called out where they sit:

1. `H19 precedence: an UNRESOLVED comment-borne target is NOT silenced
by the body` asserted exactly the row this card measures as unclearable.
Same fixture, re-judged to the direction the file's own asymmetry table
always declared: it now founds **no** row beside a body set.
2. `H19 superseded: the clause rides the UNJUDGED branch as well` used a
fixture whose body named a target that appeared in no resolution. Its
unreadable target is now the BODY's own, which is what the pin was
always about: a hygiene clause can only ride a row something in the
judged set founded.

One prose correction rides along: the summary line's H19 shortfall
clause promised that every unresolved target is named on its own card's
row. That clause was measured false once already (objectstack-ai#11218) by promising
more than the rows deliver, and a repair that narrows what a row founds
owes it a second correction — it now names the archive-only class it no
longer founds a row for.

## Ablation — from the committed fix, restored by blob hash under a trap

Mutation: the founding filter `state === 'unresolved' && carries(r)`
reverted to `state === 'unresolved'`, on disk, at `HEAD` = `ab30706ed`.

```text
HEAD blob: fe92847
BEFORE: fixed-line count = 1     AFTER: fixed-line count = 0
BEFORE: mutant-line count = 1    AFTER: mutant-line count = 2   (one pre-existing identical line in H4's prose clause)
AFTER:  on-disk blob = f764989fbeebeedd2d9920f7dd7678da0abdff1c   (differs from HEAD blob)
ABLATED self-test exit: 1 — 8 of 4583 cases failed
RESTORED: blob fe92847 == HEAD blob; git diff HEAD empty
```

Direction: RED, as predicted, and the reds are the right ones — five
negatives plus three counts:

```text
✗ H19 precedence: an UNRESOLVED comment-only target founds NO row beside a body set
✗ H19 objectstack-ai#18379: …and the unclearable row is GONE
✗ H19 objectstack-ai#18379: …⛔ with no quieter row invented in its place
✗ H19 objectstack-ai#18379: a superseded target the archive alone names founds nothing
✗ H19 objectstack-ai#18379: …and that holds for a LOCAL superseded number too
✗ H19 objectstack-ai#18379 withhold: …and the count stays the judged set's
✗ H19 objectstack-ai#18379 clauses: …and neither enters the count
✗ H19 objectstack-ai#18379: the UNJUDGED denominator never counts an archive-only target
```

The `got` value on the card's own fixture is the measured sentence,
reproduced verbatim by the ablated code:

> `pm:blocked` and 1 of 2 `Blocked-by:` target(s) could NOT be resolved
this sweep (`objectstack-ai#13515` (HTTP 404)) — the set judged against the BODY's
`Blocked-by:` set …

## Self-test

```text
node scripts/pm/check-half-states.mjs --self-test   ::  exit 0
✓ check-half-states self-test: 4583 cases pass. Batteries: H66 released queue card 183/172, H19 judged-set founding 37/34.
```

Run under `scripts/pm/os-verify-lock.sh` (`VERDICT command-exit 0`), as
were the ablated run and the live sweep.

## Live sweep — report-only, from this worktree, after the fix

`node scripts/pm/check-half-states.mjs` at 2026-09-17T12:25Z, head
`ab30706ed`, under the verify lock (`VERDICT command-exit 0`, held
366s): swept 409 open pm-/p0-labeled issues, 462 half-states, 12 H19
rows.

**Before** — anchor objectstack-ai#9857 as the scheduled patrol rewrote it at
`2026-09-17T07:50:06Z` (four H19 rows, all `UNJUDGED`, which sort above
the body's size trim): `11973`, **`11975`**, `15140`, `15214`.

> **H19** objectstack-ai#11975 — `pm:blocked` and 1 of 2 `Blocked-by:` target(s) could
NOT be resolved this sweep (`objectstack-ai#13515` (HTTP 404)) — the set judged
against the BODY's `Blocked-by:` set …

**After** — this run:

- **objectstack-ai#11975 carries no H19 row at all.** Its rows are H11
(important-parked) and H26 (transitive wait on `objectstack-ai#18336`) — the two that
were always true of it.
- **objectstack-ai#11973 still fires `UNJUDGED` on `objectstack-ai#13689` (HTTP 404)** — the same
unreadable epic, reached through that card's **body**. This is the scope
note on the card, and it is the control that proves nothing was
silenced.
- **objectstack-ai#15140 and objectstack-ai#15214 still fire `UNJUDGED`** on `objectstack-ai/cloud`
targets (HTTP 403, repo not readable to this credential) — the standing
accepted cross-repo class, untouched.
- Ordinary expiry rows still fire (objectstack-ai#15642, objectstack-ai#15768, objectstack-ai#16354): `is CLOSED …
the block has outlived its blocker`.
- The summary clause now publishes the new limit on every run: 「a target
reaching a card ONLY through the comment archive beside a body that
states its own `Blocked-by:` set is outside what H19 judges, so it rides
a row rather than founding one」.

## Gates

Derived from this worktree with `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack
scripts/pm/check-half-states.mjs` (list re-derived after the commit:
identical), every command run, each exit code captured by
redirect-then-`$?`, reconciled with `--ran`:

```text
✓ dispatch-gates --ran: 38 derived familie(s) accounted for — 38 run, 0 NOT-MEASURED
  (a DERIVED zero — all 38 recorded an exit code and none of them is 3)
```

All 38 exited **0**. In full:

```text
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-changeset-no-major.mjs --self-test :: exit 0
node scripts/check-ci-filter-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs --self-test :: exit 0
node scripts/check-scripts-symbol-anchors.mjs :: exit 0
node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0
node scripts/check-self-test-wired.mjs :: exit 0
node scripts/check-self-test-wired.mjs --self-test :: exit 0
node scripts/check-self-test-workflow-commands.mjs :: exit 0
node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0
node scripts/check-whole-set-label-write.mjs :: exit 0
node scripts/check-whole-set-label-write.mjs --self-test :: exit 0
node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0
node scripts/pm/board-snapshot.mjs --self-test :: exit 0
node scripts/pm/check-governed-queue-guard.mjs --self-test :: exit 0
node scripts/pm/sweep-closed-cards.mjs --self-test :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:bash32-floor :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:cli-command-ids :: exit 0
pnpm check:closing-target-claim :: exit 0
pnpm check:commit-card-trailers :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:entry-guard :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:parse-guard :: exit 0
pnpm check:partof-closing-keyword :: exit 0
pnpm check:pm-dispatch-gates :: exit 0
pnpm check:pm-half-states :: exit 0
pnpm check:pnpm-filter-targets :: exit 0
pnpm check:ratchet-remedy-authority :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm lint  (repo-wide, as PR objectstack-ai#18514 did) :: exit 0
```

`skip-changeset`: `scripts/pm/**` is the fast lane — nothing in any
package's `files[]` moves, and the patrol script is not published.

## Acceptance notes

- Diff is one file and confined to H19, its clauses, its summary
sentence and its pins — objectstack-ai#18372 (a new reading in the same file) queues
behind this PR untouched.
- objectui's byte-pinned copy is **not** touched; it re-syncs through
objectui#9395.
- Noted, not filed: H26 (`h26BlockOnIndefiniteTarget`) reads the same
union with no carrier split at all, so a target superseded in the body
can still found an indefinite-wait row there. It is a different row with
a different founding question and no measured instance was taken in this
act, so it is an observation, not a card. Carrier: whoever next opens
H26.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants