Skip to content

fix(pm): widen H8's inputs — branch-name delivery, closed-card residue (H22), and open-PR suppression - #11107

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-11036-h8-patrol-family
Aug 22, 2026
Merged

fix(pm): widen H8's inputs — branch-name delivery, closed-card residue (H22), and open-PR suppression#11107
os-zhuang merged 3 commits into
mainfrom
claude/issue-11036-h8-patrol-family

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #11036
Fixes #10688
Fixes #10468

The H8 patrol family — three graded cards, one predicate, one script, one PR. All
three are the same defect shape: H8's input set is too narrow. It ignored the
branch names every PR row already carries, it never revisited a card once it
closed, and it could not consult the open PRs the sweep already holds. One commit
per member.

Verified at a256ecc9 (the final commit — every gate result below was produced on
this tree, after the last commit).


1. Branch-name fallback (#11036)

h8MergedPrStillDispatched read delivery from the PR body alone — Part of #N or
a closing keyword bound to #N. A merged delivery spelled any third way was
invisible. The measured specimen: a merged PR whose body carried only a Refs
reference to card #10757, which then sat carrying pm:dispatched unreported for
~22h in a sweep that reported six other H8 rows correctly.

Every dev branch here is claude/issue-<n>-<slug> by protocol and every PR row
already carries head.ref, so the recovery costs no API call.

The card's ⚠️ is the load-bearing clause, and it shapes the implementation.
This widens the delivery relation, so the branch name is a fallback, not a
third || term: a branch cut for card N and later re-scoped would otherwise be
counted as delivering N forever, on the authority of a name nobody updated. The
body is the channel an author maintains, so a body declaring any delivery is
authoritative, and the branch name is consulted only for bodies declaring none —
exactly the population the specimen came from.

Both directions are pinned in the self-test, as the card requires:

direction case expectation
hit merged, body carries only a Refs reference, branch names the card reports
re-scope branch named for 4321, body delivers 9999 4321 stays clean

plus: the re-scoped body's own card still reports, a closing keyword for another
card also suppresses the fallback, closed-unmerged on the card branch is still not
a delivery, a non-protocol branch name delivers nothing, and the anchored reader is
pinned against CLAIM_BRANCH_SHAPE so one branch convention keeps two readers in
agreement.

2. Closed-card residue sweep + backfill (#10688), direction A

Every collector in this sweep is open-only by construction. That is right for most
items and a gap for H8, whose subject is a write that has not happened yet — while
the card is usually closed by the same merge that discharges the PR. So whether H8
ever fired was decided by a race it normally loses: once the card closes, no run
looks at it again and the duty is discharged by disappearance.

H22 adds one bounded closed reader (sort=updated, two pages — the same
convention and quota decision as the merged-PR window). Every other collector is
untouched and still open-only, so the race closes without widening the sweep.

The residue vocabulary is its own constant, PM_RESIDUE_LABELS. It is deliberately
not H13's PM_STATE_LABELS, which answers a different question ("does this
label make the card visible to a reader?") and diverges in both directions:
finding and needs-user-decision are perfectly good states to close in, while
pm:blocking is a live-work claim H13 omits. Sharing one list would have made H22
report every closed finding card on the board and miss the blocking-cache residue
entirely. The divergence is pinned in both directions.

Re-measurement — the card's population numbers are a window artifact

The card reports "129 closed carriers, 104 remaining". Re-measuring before writing,
as instructed, that figure is bounded by its own 500-most-recently-updated scan,
not the population. A label-scoped query paged past 500 closed pm:dispatched
carriers alone
and was still returning full pages back to 2026-08-13.

This is why the window cap is load-bearing rather than incidental: an unbounded
closed read would bury every other item under one-time historical residue. H22
therefore reports recent residue — where the paired write is still a live duty
someone remembers — and the deep tail is a backfill question, not a patrol question.

Backfill performed

Scoped to the same bounded window H22 actually reads, so the row's first live run
is quiet and the code and the board agree:

measure count
window swept (2 pages, most recently updated closed issues) 200
carriers found 74
cleaned (read-modify-write, per card) 74
carriers remaining in the window (re-read, both pages) 0
rate-limiting encountered none

Cleaned by label: pm:dispatched 68 · pm:queue 8 · pm:blocking 3 ·
pm:blocked 1 (counts overlap — 6 cards carried two). Page 2 was already light
because the devx cleanup recorded in the card is visible in it.

⚠️ Left undone deliberately, for the PM: the carriers older than this window —
500+ repo-wide by the re-measure above. That is far beyond the ~104 this dispatch
was sized against, and clearing it is a decision about historical metadata rather
than about a live duty. Flagged in the report rather than actioned.

3. Open-PR suppression (#10468)

H8's entire input was the merged-PR window, so it could not ask the question that
decides the answer: is there also an unmerged PR delivering this card? On a card
delivered in halves the first half's merge made the row fire, and it kept firing on
every subsequent sweep until the last half landed — pointing at the card whose
remaining work was most active, and prescribing a destructive write against it
("drop pm:dispatched"). A reader who followed that row de-labelled a card with
live work, which then read as un-dispatched and was liable to be re-dispatched: two
agents on one card, the outcome the claim protocol exists to prevent. Measured on
card #9834, whose error-counter half sat open as a draft while its duration half
had merged.

The open-PR list is already in hand from the H7/H12/H21 pass, so this adds no API
call; delivery is read through the same relation, branch-name fallback included.
Drafts are deliberately not filtered out — the specimen was one.

Implementer's call, with the caveat carried: downgrade, do not silence. Silence
would fix the harm and buy a new one, exactly as the card's caveat says — it loses
the genuine case where the last half is later abandoned, when the merged half
really is delivered and the card really is stale. So the half-delivered case emits
a distinct quieter sentence that names both sides, counts them (N of M), and says
pm:dispatched is correct here and must not be dropped. The rationale is in the
docblock, as asked. The destructive prescription now fires only when every
delivering PR has merged, and that path is pinned unchanged.


Verification

Landing class — node scripts/pm/check-governed-merges.mjs --test, its own verdict
line:

✅ NOT governed — ordinary queue landing applies to a PR with exactly this file list.

Gate families derived with node scripts/pm/dispatch-gates.mjs on the actual change
set (1 path). All 10 matched families run, each quoting its own verdict line:

gate its own verdict
check:pm-half-states (the patrol's self-test) ✓ check-half-states self-test: 838 cases pass.
check:cross-package-test-inputs OK: 13 package(s) read outside themselves, all declared…
check:entry-guard ✓ check:entry-guard: 136 scripts/ file(s) — every entry guard goes through invoked-as.mjs
check:parse-guard green (tiered report, exit 0)
check:partof-closing-keyword ✓ check-partof-closing-keyword self-test: 28 cases pass.
check-ci-filter-parity.mjs OK: all 83 declared cross-package glob(s)…
check-closing-keyword-parity.mjs OK (3 parsers agree on all 9 keywords…)
check-cross-package-test-inputs.mjs OK: 13 package(s) read outside themselves…
check-partof-closing-keyword.mjs (per-PR) run against this body — see below
check-half-states.mjs (live patrol) cannot run in this container — see below

Self-test grew 793 → 838 cases (+45 across the three members).

Ablation, member 1. The branch fallback was removed from the committed state
(return false in its place), confirmed on disk by counting both the injected
marker (1) and the removed text (0) rather than trusting the editor's exit code.
Direction observed: red, exit 1 — thrown at the Refs-specimen assertion, so
the crash preempts the tallied count rather than producing counted rows. No
rebuild leg applies: this script is executed directly from source by its own gate,
with no dist/ resolution in the path. Restore ran under an EXIT INT TERM trap
and was verified clean afterwards (git status --porcelain empty, real line back,
marker gone).

Lint, narrowed and declared. Repo-wide pnpm lint is CI's run. Narrowed to the
changed file with all three pieces of evidence: the population is read from eslint's
own config (--print-config resolves for this file), the count is read from
--format json (1 file, 0 errors, 0 warnings), and parserOptions.project is
null — type-aware linting is not enabled here, so a one-file diff cannot move the
verdict of any untouched file.

Not run, and why: the live sweep (node scripts/pm/check-half-states.mjs). Its
own --probe reports PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential and exit 3; the file header already records that the live
sweep cannot run inside a PM session container. The H22 reader is therefore covered
by self-test and by the backfill's live read-back above, not by a local live run.

No changeset: scripts/ only, this PR publishes nothing. Carrying skip-changeset.


Generated by Claude Code

claude added 3 commits August 22, 2026 16:59
…y declares none

`h8MergedPrStillDispatched` read delivery from the PR body alone — `Part of #N`
or a closing keyword bound to `#N`. A merged delivery spelled any third way was
invisible: the measured specimen was a merged PR whose body said only
`Refs #10757`, leaving that card carrying `pm:dispatched` unreported for ~22h in
a sweep that reported six other H8 rows correctly.

Every dev branch here is `claude/issue-<n>-<slug>` by protocol and every PR row
already carries `head.ref`, so the recovery costs no API call. It is spelled as
a FALLBACK rather than a third `||` term, because widening the delivery relation
has a cost: a branch cut for card N and later re-scoped would otherwise be
counted as delivering N forever, on the authority of a name nobody updated. The
body is the channel an author maintains, so a body declaring ANY delivery is
authoritative and the branch name is consulted only for bodies declaring none.

Both directions are pinned in the self-test per the card's binding caveat: the
`Refs #N` specimen shape reports, and a re-scoped branch does not false-fire.

Fixes #11036
…tive drop on half-delivered cards

`h8MergedPrStillDispatched`'s entire input was the merged-PR window, so it had
no way to ask the question that decides the answer: is there ALSO an unmerged PR
delivering this card? On a card delivered in halves the first half's merge made
the row fire, and it kept firing on every subsequent sweep until the last half
landed — pointing at the card whose remaining work was most active, and
prescribing a destructive write against it ("drop `pm:dispatched`"). A reader
who followed it de-labelled a card with live work, which then read as
un-dispatched and was liable to be re-dispatched: two agents on one card.
Measured on #9834, whose error-counter half sat open as draft #10226 while its
duration half had merged as #10004.

The open-PR list is already in hand from the H7/H12/H21 pass, so this adds no
API call; delivery is read through the same relation, branch-name fallback
included. Drafts are deliberately not filtered — the specimen was one.

The half-delivered case DOWNGRADES rather than falling silent. Silence would fix
the harm and buy a new one, per the card's binding caveat: it loses the genuine
#8683 case where the last half is later abandoned. The quieter sentence names
both sides, counts them, and says `pm:dispatched` is correct and must not be
dropped. The destructive prescription now fires only when every delivering PR
has merged, and that path is pinned unchanged.

Fixes #10468
Every collector in this sweep is open-only by construction, and for most items
that is right. It is a gap for H8 specifically: H8's subject is a write that has
not happened yet, but the card is usually closed by the same merge that
discharges the PR — often by a `Closes #N` in the same instant. So whether H8
ever fired was decided by a race it normally loses. Once the card closes, no run
looks at it again and the duty is discharged by disappearance.

Measured at filing: 129 of the 500 most recently updated closed cards carried a
live `pm:` label, 118 of them `pm:dispatched` — the signature of exactly the
write H8 was built to catch, unmet at scale because the card closed first.

This is direction A: ONE bounded closed reader (`sort=updated`, two pages, the
same convention and quota decision as the merged-PR window), every other
collector unchanged and still open-only, so the race closes without widening the
sweep. The window is the stated boundary and it is load-bearing — a 2026-08-22
re-measure paged past 500 closed `pm:dispatched` carriers repo-wide, so an
unbounded read would bury every other item under one-time historical residue.

The residue vocabulary is its own constant. It is deliberately NOT H13's
`PM_STATE_LABELS`, which answers a different question ("does this label make the
card visible to a reader?") and diverges in both directions: `finding` and
`needs-user-decision` are fine states to close in, while `pm:blocking` is a
live-work claim H13 omits. Sharing one list would make H22 report every closed
finding card and miss the blocking-cache residue entirely; the divergence is
pinned in both directions.

Fixes #10688
@os-zhuang os-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 22, 2026 — with Claude

Copy link
Copy Markdown
Contributor Author

PM 验收 — ACCEPT

复核:席位 session_01MsbKEG4LtERSLaDrbehM3e 全 diff 对抗性审毕,三成员(#11036 分支名回退、#10468 open 侧降级、#10688 H22 有界 closed 读者)逐项吻合定级意图。关键设计点核验:回退 precedence(body 声明任何交付即权威)双向 pin;open 侧只软化不自造行、draft 不过滤;PM_RESIDUE_LABELS 与 H13 集合分立论证双向成立且互斥性入测。自测 793→838(+45),消融红向观察 + 双读恢复验证,pnpm install 副作用被门抓出并在推送前重建剔除(申报在案)。backfill:窗内 74/74 清零,双页复读确认。

开放问题裁定(席位纪律内,报维护者知悉):深尾 500+ closed 载体采 A(留置)——H22 巡逻近期活性 residue,深尾属一次性回填问题,B(专卡预算化回填)备选留维护者裁量;pm:retriageA(先测后扩),排除已注释在常量处。

落地类:非治理面(check-governed-merges --test 判定在案)——普通队列流程,本席 ready + auto-merge 布防,队列 ref 回读验证后合并;合并后剥离三卡 pm:dispatched


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 22, 2026 17:13
@os-zhuang
os-zhuang enabled auto-merge August 22, 2026 17:13
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 22, 2026
Merged via the queue into main with commit b302aab Aug 22, 2026
33 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-11036-h8-patrol-family branch August 22, 2026 17:40
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32587651072 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 5.43s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️ 本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 82 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment