fix(pm): the half-state patrol's seat legs read the triage post and the newest page — H44 reaches a vacant or Routine seat, H32 / H38 take their marker from the last page - #18332
Conversation
…18324) `h44NeedsSeatComments` was `pm:seat` + `seatIsHeld`, and `seatIsHeld` excludes a `🟢 Routine` holder by name and every non-🟢 status. The triage seat runs as a Routine or sits vacant most of the time — it read `🔴 空缺` on the live board at the filing — so H44, H56 and H64 read NOTHING on the one post H44's own header names as its worked example, while H65 read it every run through `h65IsTriageSeatPost`. The gate now admits the triage post through that same predicate, beside the held leg rather than instead of it: the file keeps ONE reading of 「is this the triage post」. Measured cost: ZERO additional requests, because H65 already buys exactly that page every run through the shared `seatPostRowsFor` memo, so the widening only moves which row asks first. ⛔ Not a vacant-seat posture, and that is measured rather than assumed: all three rows accuse a COMMENT and name its author, never the post's holder. On the vacant triage post's newest page (18 comments, 2026-09-10 → 2026-09-15) H44 files one carrier row naming 10 offending comments, H56 files 0, H64 files 0. H32's population is untouched — its subject is an accusation that a named holder is not working, which is what the held gate is for. Self-test 4235 → 4245 cases. The H65 case that pinned the old behaviour is rewritten in place, not deleted, with the superseded statement quoted above it. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr
…ge (#18325) The `seatMarkers` gather captured `commentRowsFor`'s page-less FIRST page, and GitHub serves issue comments OLDEST-FIRST, so `latestSeatMarker` returned the newest row of page 1. Measured on the live board: #6023's 225-comment thread yielded 2026-08-27T16:38:35Z while its real latest marker was 2026-09-14T11:41:59Z, 18 days newer; #6367 yielded 2026-09-04 against a real 2026-09-10. That row is H32's idle clock AND carries H32's wait exemption, and it is H38's `T_seat` — so the one reading on a seat post that must be the newest was the oldest page the endpoint serves. The gather now takes the window through `seatPostRowsFor`, the file's one declaration of a seat post's window. It buys nothing new: this population is a SUBSET of H44's seat leg, the memo is shared, and asking here only makes the gather loop the first asker. The page-less fetch it replaces was itself wasted — H44's leg overwrote that cache entry a pass below. `latestSeatMarker`'s contract is unchanged; a null page is left out of `seatMarkers` exactly as the throw was.⚠️ H38's live verdicts rarely move, and the self-test pins why rather than leaving it to be discovered: `seatPostLastEventMs` maxes the rows against the post's own `updated_at`, which GitHub bumps on every new comment, so the page choice decides `T_seat` exactly where that field cannot — a comment EDITED after the post's last bump. `seatWindowAudit`'s reader pin is rewritten 2 -> 3 with the superseded statement quoted above it; its page-less (1), commentPaths (4) and purchase-site (1) readings are unchanged. Self-test 4245 -> 4261 cases. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr
Takes in PR #18326 (the H44_READING_TIMESTAMP region and its cases), which landed on main while this branch was in flight. Different regions of the same file; no conflict. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr
The live run on the widened leg printed `5 of 5 HELD seat post(s)` while one of those five was the VACANT triage post — the clause described its own count wrongly on every run. Measured against `origin/main`: the leg reads 4 of 4 there and 5 of 5 here, and the fifth is #6015. The clause now names both legs. Its numbers are unchanged, and the existing pin that asserted the HELD wording is rewritten in place with the superseded string quoted above it, plus one new pin on the two-leg sentence. Split from the first commit of this branch because the defect was only visible in a live patrol run taken after it — the self-test pins the clause's substrings and both readings are legal English. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr
Contract reviewServed-tier: Reviewing seat: ① Derived judgments
② Semver levelNot applicable — nothing published moves; ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Landing provenance — skills seat, session Generated by Claude Code |
… skip is not a failure, and a check reports the skips outside the declared roster (objectstack-ai#18357) Fixes objectstack-ai#18308 ## What `.claude/skills/pm-dispatch/SKILL.md` :629 read 「入队资格 = PR 上每一个 check 全绿,⛔ 不是 required 子集;required 集是队列强制的地板。」 — a bar no landing on this repository can satisfy, because `skipped` is the ordinary conclusion of a path-filtered job and the charter defined no state for it. Measured over the ten most recent landed heads (below), every head carried 8–19 skipped check-runs beside its successes; the seat's landings read 「success = green, skipped = not failed」 by an unstated convention. Two changes, both inside the claimed file surface: 1. **SKILL.md :629, re-keyed in place** (112 B → 119 B, ≤120 B; 812 → 812 lines; no issue number): > `- 入队资格 = 每个 check 为 success 或预期 skip(名单:check-expected-skips.mjs),⛔ 不是 required 子集。` The bar now names the two states a check may be in — `success`, or an expected skip — and points at the one machine-readable roster of expected skips. The third clause of the old line (「required 集是队列强制的地板」) did not fit the byte ceiling and is carried by AGENTS.md §7 (「the queue enforces only the required set」); the ⛔ clause is kept verbatim. 2. **`scripts/pm/check-expected-skips.mjs`** (new; `package.json` gains `check:pm-expected-skips` = its `--self-test`): given `--pr N` or `--head SHA` (or a pre-fetched payload via `--check-runs-json FILE|-`), it reads the head's check-runs and judges every `skipped` run against a roster declared once, in the file, as data with a one-line reason per row. Exit register: **0** every skip is in the roster · **4** a skip is outside it (each named and classified: a filter miss, or a dependency skip when the same check suite holds a failed run; a raw `matrix` template in the name is read as "skipped before matrix expansion, i.e. a job-level gate — never a workflow-level `paths:` filter, which creates no check-run at all") · **3** NOT MEASURED (unresolvable sha, 404, network, no check-runs on the head, or a check-run still running — the skip set is not final). Report-only; the self-test pins structurally that the file carries no `method:` key and imports no writer. The roster is **tied to the workflows, not remembered**: `--self-test` parses each row's workflow with the `yaml` package and asserts the job exists, carries the row's name, carries an `if:`, that the `if:` spells the declared gate (`needs.filter.outputs.X != 'false'`, the `github.event.action` exclusion, or the label literal), and — for ci.yml rows — that the `filter` job's output keeps its `|| 'true'` widening, which is what makes "the merge-queue build runs it" true. The audit is driven red in the self-test on a deleted, renamed, un-gated and re-gated job, a lost widening and an unreadable workflow. ## The roster (11 names), measured over ten landed heads | name | workflow › job | mechanism | over the ten heads | |---|---|---|---| | `Build Core` | ci.yml › build-core | `filter` output `core` said false; REQUIRED context, judged on the queue build | skipped 10/10 | | `Temporal Conformance (live PG + MySQL)` | ci.yml › temporal-conformance | same, REQUIRED context | skipped 10/10 | | `Dogfood Regression Gate (${{ matrix.shard }}/3)` | ci.yml › dogfood | same; raw matrix template = pre-expansion name (the aggregate `Dogfood Regression Gate` runs `if: always()`, never skips) | skipped 10/10 | | `Dogfood Verify CLI` | ci.yml › dogfood-verify | same | skipped 10/10 | | `Test Core (${{ matrix.shard }}/6)` | ci.yml › test | `core` OR `crosspkg` both false (scripts/** is in `crosspkg`, so scripts/pm heads RUN it) | skipped 3/10 — only the .md-only heads | | `Build Docs` | ci.yml › build-docs | `filter` output `docs` | skipped 10/10 | | `Console Pin Gate` | ci.yml › console-pin | `filter` output `console` | skipped 10/10 | | `Check PR Size` | pr-automation.yml › pr-size | `if:` excludes `labeled` / `unlabeled` / `edited` events; each event is its own run on the same head | skipped 9/10, success beside it 10/10 | | `Auto Label` | pr-automation.yml › auto-label | same | skipped 9/10, success beside it 10/10 | | `Check Changeset` | pr-automation.yml › changeset-check | `if:` skips a PR carrying `skip-changeset` | skipped 10/10 (every head carried the label), success beside it 9/10 (the run before the label) | | `Packed-tarball smoke (opt-in)` | pack-smoke-optin.yml › pack-smoke | opt-in by `needs:pack-smoke` | skipped 10/10 | Never skipped on any of the ten heads (and carrying no `if:`): `Lint & Repo Gates`, the four `Type Check ·` lanes, `TypeScript Type Check`, `Test Core` and `Dogfood Regression Gate` (the aggregates), `Governed Surface Queue Guard`, `filter`, the four claim/keyword guards, `Check Documentation Links`, `Close issues referenced in other repositories`. Workflows with a workflow-level `paths:` filter (`half-state-patrol.yml`, `board-snapshot.yml`) produce no check-run at all on a non-matching head — they are absent on 6 of the ten heads, never `skipped` — which is the measured basis for the "a skipped check-run is never a `paths:` filter" reading. ## Reverse verification (all at `7a1f99ea`) | leg | result | |---|---| | `--head` on the ten landed heads objectstack-ai#18298 · objectstack-ai#18307 · objectstack-ai#18311 · objectstack-ai#18315 · objectstack-ai#18316 · objectstack-ai#18322 · objectstack-ai#18326 · objectstack-ai#18327 · objectstack-ai#18328 · objectstack-ai#18332 | **exit 0 on every one**; accepted skips per head: 11 · 12 · 11 · 19 · 12 · 8 · 11 · 18 · 18 · 11, every name in the roster; e.g. objectstack-ai#18322 (the 8-skip head): `Build Core`, `Build Docs`, `Check Changeset`, `Console Pin Gate`, `Dogfood Regression Gate (…/3)`, `Dogfood Verify CLI`, `Packed-tarball smoke (opt-in)`, `Temporal Conformance` | | constructed fixture: the real objectstack-ai#18322 payload with `Lint & Repo Gates` mutated to `skipped` | **exit 4**, naming `Lint & Repo Gates (check suite 94780297729)` and classifying it `filter-miss` | | garbage sha `--head deadbeef…deadbeef` | **exit 3** — `NOT MEASURED — HTTP 422 — the API cannot resolve that sha` | | `--pr 18315` (the head is looked up through the proxy) | exit 0, `19 skipped check-run(s), every one in the roster`; `--pr 18308` (an issue number, not a PR) → exit 3 (HTTP 404) | | `--self-test` | 99 cases pass, offline (the roster's truth on the live workflows and its audit driven red six ways; the judge on the measured 39-run objectstack-ai#18315 head and on fixtures for 0 / 4 / 3; read classification; argv; the real CLI on payload files incl. `--json`; the structural pins) | | SKILL.md ratchet | `wc -l` 812 → 812; :629 112 B → 119 B; `check-skill-line-ratchet: SKILL.md is 812 lines (ceiling 812; headroom 0)` | ## Gates (local, at `7a1f99ea`) `node scripts/pm/dispatch-gates.mjs --commands .claude/skills/pm-dispatch/SKILL.md scripts/pm/check-expected-skips.mjs package.json` derived 45 commands; all 45 were run with the exit captured by redirect, and `--ran` reconciles: `✓ dispatch-gates --ran: 45 derived famil(ies) accounted for — 40 run, 5 NOT-MEASURED (5 DERIVED from a recorded exit 3)`. The five NOT MEASURED are the `dist/`-reading families on an unbuilt tree (`check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content`, `@objectstack/lint check:doc-formula-expressions` — each prints `PREREQUISITE NOT MET`); this diff touches no package, so no build closure is owed locally and CI runs them built. The `pnpm check:pm-dispatch-gates` battery was not derived, so it was not run. Named gates, verdict lines quoted: `check-skill-line-ratchet: SKILL.md is 812 lines (ceiling 812; headroom 0)` · `check-skill-id-lint: 27 file(s) clean` · `check-skill-frame-sync: the one declared copy of the decision frame is internally coherent` · `check-self-test-wired: every one of the 212 script(s) CI runs that ship a --self-test has that self-test run by CI` (the new script is not in that population — see Acceptance notes) · `check-nul-bytes: OK (scanned 8707 text file(s))` · `check-governed-prose: 2 instruction surface(s) name all 5 registered governed surfaces` · ESLint (`--no-inline-config`) on the new file: exit 0 · `check-governed-merges.mjs --test .claude/skills/pm-dispatch/SKILL.md`: **GOVERNED** (`.claude/**` ×1), exit 3 as designed. `check-clause2-carriers.mjs --pair` is run once this PR exists and its reading goes in the report comment. ## The one design choice, on the four axes: a roster declared in the check vs. deriving expectedness live from the workflows' `paths` filters - **实际业务需求** — the measured need is name-level: 31 landings this shift and the ten heads above were judged by "is this skipped name one that always skips?", and zero of them needed a diff-level answer. The diff-level question ("should `Build Core` have run on THIS diff?") is already answered for the required family by the platform: on `merge_group` ci.yml's `filter` widens every output to `'true'` (the `|| 'true'` half of the filter contract, now pinned by this check's self-test), so the family runs on the merged tree before `main` moves. A live derivation would answer a question nobody measured a need for, at the cost below. - **项目长远合理性** — a roster is a declaration that can rot; a live derivation is a second evaluator of the platform's own semantics (dorny/paths-filter's picomatch dialect, GitHub's expression language, matrix name templates, per-event runs) that can drift from the real evaluator. Both are drift; the roster's drift is made LOUD here (every row is pinned to its live job, name, `if:` and gate spelling — a rename or re-gate reddens CI), while an evaluator's drift is silent by construction (a wrong glob yields a confident "expected"). Contract-first: the workflow file is the contract, and the roster is a checked reading of it, not a copy of its path lists. - **防 AI 写代码犯错** — the roster makes the wrong move structurally hard: a new gated job's first skip is exit 4 until someone adds a row WITH its mechanism, and a row that names a job the tree does not gate is red. A live evaluator is where an AI would quietly mis-implement glob semantics and produce the false green this tree refuses everywhere else (the "could not read" ≠ "clean" class). The declared-vs-delivered line is kept: the check advertises the name question only, and says so in its header and report. - **创业阶段不扩散需求** — the roster is ~11 rows of data and one audit; live derivation is a YAML-expression evaluator with parity tests against GitHub. No pull exists for the latter; if a rostered required job is ever found skipped on a diff inside its filter, that measurement is the card that would justify it. **Recommendation: the roster in the check (implemented).** Should the seat prefer live derivation, nothing here blocks it — the roster rows already carry `workflow`, `job` and the gate's outputs, which is the input a derivation would start from. ## Acceptance notes - **Self-test wiring.** `check:pm-expected-skips` exists in `package.json` (mirroring the report-only siblings), but no workflow names it and lint.yml was outside this card's file surface, so `check-self-test-wired` (correctly) does not count it and CI does not run its 99 cases. The completion is one lint.yml step beside the other `check:pm-*` steps (`run: pnpm check:pm-expected-skips`); left to the seat — 承接者:the skills seat, on this PR or a sibling. Noted, not filed. - **:629's floor clause dropped for the byte ceiling** (「required 集是队列强制的地板」); AGENTS.md §7 carries the fact. Noted, not filed. - **Exit 4 judges skips only.** Other conclusions on the head (`failure`, `cancelled`, `neutral`, …) are printed loudly under `other conclusions` and do not move this check's exit; the bar's success half is read from the same listing. A malformed `--head` (non-hex) is a usage error (exit 2), a well-formed sha the API cannot resolve is exit 3. Noted, not filed. - **The card's five-name family was a subset.** The measured recurring family is eleven names (six ci.yml `filter`-gated jobs the card did not list, including two REQUIRED contexts); the card's citation of a "platform-readings discipline (a skip is not a pass)" has no verbatim carrier — the nearest lines are AGENTS.md §7 (「Green means the gate-carrying jobs' conclusion is success」) and `references/review-checklist.md:43`. Recorded in the report, no card. - objectstack-ai#18349 is not addressed here; it holds :513 / :523 of the same file (region-level parallel). `origin/main` did not move under this branch after cut (`ceb6b5fb`). ## 维护者速读(草稿) **改了什么**:入队资格这一行改成「每个 check 为 success 或预期 skip」,并新增一个只读的检查脚本 `scripts/pm/check-expected-skips.mjs`:给它一个 PR 号或提交 SHA,它读出该提交上所有 check,凡是 `skipped` 的都对照脚本内声明的「预期 skip 名单」(11 个名字,每个带一句为什么会 skip 的机制),名单外的 skip 会被点名并退出码 4;读不到就退出码 3,绝不当作通过。 **为什么改**:原来的「每一个 check 全绿」在本仓库任何一个 PR 上都做不到——路径过滤的 job 本来就以 `skipped` 结束,实测最近十次落地每次都有 8–19 个 skip。席位一直靠「记得哪些通常会 skip」在判断,而真正要分辨的是「预期 skip」与「本该跑却没跑」。现在名单是机器可读的,并且自测会把名单逐条对照真实 workflow 文件校验(job 存在、名字一致、带条件、条件拼写一致),名单不会悄悄过期。 **风险与代价(含回滚)**:规则层只改一行(≤120 B、行数 812 不变);脚本只读不写、不接入任何门禁,CI 不因它变红。名单是名字层面的判断,不回答「这个 diff 是否本该触发某个 job」——必查项由合并队列在合并树上全量重跑兜底,这一点写在脚本头部。回滚 = revert 本 PR。 **席位意见**:(留空,席位定稿) **你要做的**:本 PR 触及 `.claude/**`(规则层),需要你的 APPROVED;之后由席位落地。是否把该脚本的自测接进 lint.yml(一行 `pnpm check:pm-expected-skips`)由席位决定,本 PR 未动 lint.yml。 --- _Generated by [Claude Code](https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr)_ Co-authored-by: Claude <noreply@anthropic.com>
Two defects in one file,
scripts/pm/check-half-states.mjs, both of the same shape: a seatleg reading a seat post through the wrong population or the wrong window while the file
already declares the right one. Each is fixed by pointing the leg at that declaration.
1 — H44's seat leg could not reach the triage post it was written for
h44NeedsSeatCommentswaspm:seat+seatIsHeld, andseatIsHeldexcludes a🟢 Routineholder by name and every non-🟢 status. The triage seat runs as a Routine orsits vacant most of the time — it read
[PM seat] triage (objectstack-wide) — 🔴 空缺 …throughout this work — so H44, H56 and H64 read NOTHING on the one post H44's own header
names as its worked example (「carrier of the round-open marker that is this card's worked
example」), while H65 read it every run through
h65IsTriageSeatPost, which carries noheld gate.
The gate now admits the triage post through that same predicate, beside the held leg
rather than instead of it, so the file keeps ONE reading of 「is this the triage post」.
Measured cost: zero additional requests. H65 already buys exactly that page every run
through the shared
seatPostRowsFormemo, so the widening only moves which row asks first.⛔ No vacant-seat posture is owed, and that is measured rather than assumed. All three
rows accuse a COMMENT and name its author; none names the post's current holder, so
「nobody is on the clock」 changes nothing about whether an artefact on the thread states a
reading with no time on it. On the vacant triage post's newest page (18 comments,
2026-09-10T23:26:55Z → 2026-09-15T15:54:40Z) H44 files one carrier row naming 10 offending
comments, H56 files 0 and H64 files 0 — confirmed offline before the change and reproduced
exactly by the live run after it.
The design choice, on the four axes
Option A was to widen the gate to the triage post; option B was to keep the held gate and
rewrite the header to say the seat leg reaches only HELD posts. A, on all four axes —
the dispatching seat's reading is confirmed, not refuted.
grammar flags, 0 of them visible to the row today, on the busiest artefact producer on
the board (818 comments, 18 of them in the last five days). B leaves a report-only row
structurally unable to observe its own subject, which is dead letter.
h65IsTriageSeatPostfor the population,
seatPostRowsForfor the window). A hands the leg those declarations;B is the workaround shape, correcting prose to match behaviour nobody wants.
stating a reading with no time on it or naming no session, and the triage post is where AI
seats write most. Declaring a purpose in the header that the runtime does not honour is the
anti-pattern this axis names. The held gate is an argument about ACCUSING A NAMED HOLDER of
idleness — H32's subject, not this row's; inheriting it was the category error.
one new finding on the live board. It makes an existing declared row reach the population
its own header already claims, in the narrow spelling already in the file. 声明而未兑现是
实现缺口,补实现或退役 — the implementation is supplied. Immediate, no staged window.
2 — H32 and H38 read a seat post's OLDEST comment page
The
seatMarkersgather capturedcommentRowsFor's page-less FIRST page, and GitHub servesissue comments oldest-first, so
latestSeatMarkerreturned the newest row of page 1. That rowis H32's idle clock and carries H32's wait exemption, and it is H38's
T_seat.Measured on the live board:
On #6023 the wait exemption was being judged on an August comment. The gather now takes the
window through
seatPostRowsFor. It buys nothing new — this population is a subset ofH44's seat leg, the memo is shared, and asking here only makes the gather loop the first
asker; the page-less fetch it replaces was itself wasted, because H44's leg overwrote that
cache entry a pass below.
latestSeatMarker's contract is unchanged, and a null page is leftout of
seatMarkersexactly as the throw was.discovered:
seatPostLastEventMsmaxes the comment stamps against the post's ownupdated_at, and GitHub bumps that field whenever a comment is added, so the post's ownfield already dominates. The page choice decides
T_seatexactly whereupdated_atcannot— a comment EDITED after the post's last bump, which is the one 「seat writing home」 signal
that lives only in the rows. Two self-test cases pin both halves of that.
Reverse verification
Ablation, per card, from the committed state (mutation proven on disk by blob hash before
each reading; restored with
git checkout HEAD -- <path>, restore proven by an emptygit diff HEADand a blob hash equal to HEAD's):H44 gate: ⭐ a VACANT triage post IS fetched — the held leg alone made this row dead letter on it,H44 gate: ⭐ …and a 🟢 Routine triage post too, which seatIsHeld excludes BY NAME,H65 scope: ⭐ H44's seat leg now REACHES the Routine triage post, through this very predicate.seat window: …and taken through the memo by more than one row, which is what makes it shared(got 2, want 3).
this, and the file says why in
seatWindowAudit's own header — the window lives insidesweepInto, which takes no injectable transport, so the property a spy would prove has to beproved on the text. The other 16 cases of this card pin the CONTRACT (which marker and which
T_seateach window yields) at function level and stay green under the ablation byconstruction. That is the designed instrument, not a gap discovered late.
seatWindowAudit, before → after:pagelessPaths1 → 1 ·commentPaths4 → 4 ·purchaseSites1 → 1 ·readers2 → 3. Only the reader count moves, and its pin isrewritten in place with the superseded statement quoted above it.
Live patrol,
origin/mainde9c319avs this head, both exit 0, same corpus both runs(408 open pm-/p0-labeled, 507 unscoped, 13 open PRs, 683 recently-merged PRs):
Total half-states 476 → 477. The one row that moved is
H44 #6015— the vacant triagepost — naming comment
5626818520as the oldest of 10 offending comments on it, which isthe offline prediction reproduced exactly. No row was lost. Coverage clause: the seat leg
reads
4 of 4onorigin/mainand5 of 5here; H44's corpus 1488 comments across 398threads → 1506 across 399, the 18 added being exactly the triage post's newest page; H32's
3 of 3and H65's1 of 1are unchanged.5 of 5 HELD seat post(s)while one of the five was the vacant triage post. Both readingsare legal English, so only a live run could catch it. The clause now names both legs; the
pin that asserted the HELD wording is rewritten in place, not deleted.
Self-test: 4235 on the old base → 4245 after card 1 → 4261 after card 2 → 4269 after
taking
mainin (PR #18326's 8 cases) → 4270 after the coverage-clause pin. Three pinswere rewritten in place rather than deleted, each with its superseded statement quoted above
it: the H65 scope case that asserted H44's gate reads
falseon a Routine triage post, theaudit's reader count, and the H44 coverage clause's HELD wording.
Gates
All 41 commands derived by
node scripts/pm/dispatch-gates.mjs --commands scripts/pm/check-half-states.mjswere run at this head and reconciled with
--ran; the batterypnpm check:pm-dispatch-gatesis among them. ESLint on the file is clean, and the file carries no control bytes.
Acceptance notes
Out of scope, noted and not filed: the seat-post window is now correct for every reader, but
the property is only observable from source text, because the window lives inside
sweepIntowhich takes no injectable transport — a spy-able seam would let a case assert the gather's
window directly instead of counting call sites. Successor: whoever next edits
sweepInto'stransport; nobody is queued on it today, and inventing the seam under these two cards would
have widened the surface well past them.
Non-governed (
scripts/pm/**) — left DRAFT for the owning seat to land in-seat after review.No reviewers requested, no auto-merge armed, no ccr route touched.
skip-changeset: nothingpublished moves —
scripts/pm/**is not in any package'sfiles[].Fixes #18324
Fixes #18325
Generated by Claude Code