Skip to content

Report the dead claim nobody can see: H27, and the post-kill recovery lines it points at - #11315

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-11248-dead-claim-predicate
Aug 23, 2026
Merged

Report the dead claim nobody can see: H27, and the post-kill recovery lines it points at#11315
hotlong merged 2 commits into
mainfrom
claude/issue-11248-dead-claim-predicate

Conversation

@claude

@claudeclaudeBot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes#11248
Part of #11109

⚠️Governed surface — this PR touches .claude/**, so it stays a DRAFT, merges by a human only, and must NOT be put on auto-merge or into a merge queue.

What this adds

H27 — the claim is perfect and the claimant is dead. A dev agent killed mid-task leaves a textbook-correct claim: pm:dispatched, assignee set, a first-line Claim: comment, and a named branch that exists on the remote. Every predicate in check-half-states.mjs passed on that shape, so the card read as healthy in-flight work indefinitely.

Its danger is not that it goes unreported but that it reads as healthy: the next PM's round-open mutual-exclusion read looks for the latest non-self Claim: on a lane's dispatched cards, so it treats a dead claim as a live claim by another session and stays off the card. The protocol's own mutual-exclusion mechanism turns one dead agent into a lane-wide block.

The finding that shaped the design: H20 misses this BY CONSTRUCTION

H20 is the near neighbour and it keys on no remote ref at all. .claude/agents/os-dev.md rule 1 makes pushing the empty branch the first action of the task (a write-route probe, before any edit) — so a protocol-compliant agent that dies at any point after its first minute still leaves a ref, and lands outside H20's population entirely. The better the dev follows the protocol, the more invisible its death. That is not an oversight in H20; it is a consequence of a protocol step that is independently correct and worth keeping.

So the two rows are disjoint by construction — H20 fires only when no branch resolves, H27 only when one does — and neither could be widened into the other without losing the property that makes it safe. Both directions are pinned on one fixture in the self-test.

The conjunction, and why each term is load-bearing

termwhy
pm:dispatchedthe card still claims to be in flight
newest Claim: older than ~24hthe protocol's OWN stale line, quoted not invented
a claimed branch EXISTSotherwise it is H20's row
NO branch moved since the claimnothing was pushed for this dispatch
no PR delivers the cardneither open nor within the merged window

H20's docblock explicitly refuses to key on "no PR yet", for a measured reason: a dev inside a long build legitimately has a ref and no PR for over an hour. That objection is answered here by both remaining terms rather than by the threshold alone — a dev 24h in with commits landing is excluded by branch activity, and a dev with a PR open is excluded by delivery. What is left is a branch that has not moved since it was claimed, with nothing to show for a day.

The threshold is quoted, not measured. The filing card is explicit that the fleet has 2 liveness samples and that is not a distribution. So the row mechanizes SKILL.md's existing 死认领回收 line (「认领 >~24h」) — the same 24h the seat-post patrol already calls 「与既有回收线同一条」. One number in the protocol, two readers.

Report-only, and specifically NOT a reclaim

The protocol's reclaim rule applies to a branch that does not exist, and it states 「有带提交活分支的认领永不回收」. This row fires on branches that do exist, so it prescribes the recovery inspection and never an assignee drop — reporting a card is not reclaiming it, and the sentence says so in as many words. It also deliberately under-reports the dev that pushed one commit and then died (its branch moved after the claim): that is precisely the card the protocol protects, and the docblock states the gap rather than leaving it to be discovered.

Cost: zero extra requests

H27's 24h population is a strict subset of H20's 60-minute one, so every card it can speak about was already probed. The shared probe now reads /repos/{o}/{r}/branches/<b> instead of /git/ref/heads/<b> — the same underlying ref, so H20's three states (exists / absent / unreadable) are untouched, but the head commit date rides in on a payload the sweep had already paid for. That is H26's own "FREE" shape. The summary line now reports the pair as serving both rows, so a reader seeing a quiet H27 knows what was read.

⚠️One honest caveat for the reviewer: the endpoint swap is on the live path, and the live sweep cannot run in a seat container (the transport fact this file's own header documents), so the offline self-test cannot pin it in either direction. Its safety rests on the two endpoints resolving the same ref with the same 200/404 semantics, and on the derived state field being computed identically. Everything downstream of the probe is pinned.

The Part of #11109 half — premise falsified, and no row was added

The dispatch asked for the patrol half of #11109 only if not already covered. It is already covered, by two merged rows, so this PR deliberately adds nothing for it. Adding a duplicate would be growth for no catch.

#11109 asks foralready merged asevidence
pm:blocked whose body and comments carry no Blocked-by: lineH4h4BlockedNoBlockedBy(issue, commentBodies) reads both channels (since the #8941/#10061 widening) and returns the "NEITHER channel" sentence
sibling: Blocked-by: target already closedH19h19BlockOutlivedBlocker — "the block has outlived its blocker"

The regression pins for #11109's exact shape are already in the self-test and were not written by this PR: H4: neither channel -> finding, H4: an empty comment thread is a real reading, not an unconsulted one, plus the decorated/bulleted/bolded variants and the cross-repo case.

I also re-measured the specimen the card was filed on, #11017: it is now closed, carries no pm:* label, has no Blocked-by: in its body but does have one in a comment — i.e. exactly the two-channel shape H4 already clears. The card's premise ("this is one more predicate the patrol needs") was true when filed against a body-only reading and is no longer true against current main.

#11109 remains open after this PR — its wording half lands separately in the wave-4-B fold, and that is the half that ends the card.

Verification

Self-test is the verification surface (the live sweep cannot run in-container — see the script header's transport note).

node scripts/pm/check-half-states.mjs --self-test
✓ check-half-states self-test: 1058 cases pass. (997 on origin/main — +61)

Reverse verification, both legs, mutation confirmed on disk. Ablated the branch-activity term (if (moved.some((m) => m === true)) return null;if (false) return null;), confirmed the anchor count went 1 → 0 and the injected marker 0 → 1 before reading any result. Predicted direction: red. Observed: red, and exactly the three predicted cases —

✗ H27: a branch that moved AFTER the claim -> clean
✗ H27: one moved branch among frozen ones clears the card
✗ H27: activity is measured against the CLAIM, not the threshold
✗ check-half-states self-test: 3 of 1058 case(s) failed.

The mutation script carried a trap … EXIT INT TERM; the restore leg was verified independently (marker absent, anchor restored, tree clean, 1058 pass).

The three existing summary: pins that broke on the reworded summary line were caught by the suite, not discovered by hand — they are updated, and the new clause is pinned.

Gates

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no hand-written path list — the script takes its own change set from the merge base). 17 families matched; 16 run locally and are green, at b2c8679c:

check:cross-package-test-inputs · check:doc-authoring · check:doc-formula-expressions · check:entry-guard · check:parse-guard · check:partof-closing-keyword · check:pm-governed-merges · check:pm-half-states · check:pm-skill-id-lint · check:pm-skill-ratchet · check:pnpm-filter-targets · check:skill-frame-sync · check-ci-filter-parity · check-closing-keyword-parity · check-cross-package-test-inputs · check-nul-bytes

The 17th, scripts/check-partof-closing-keyword.mjs invoked directly, refuses to give a verdict without PR_BODY/PR_NUMBER — correctly, it is a wiring failure and not a verdict. It was run against this body before pushing, and passes.

Ratchet

.claude/skills/pm-dispatch/references/dispatch-runbook.md was at its ceiling (243/243). Four lines added — the two this card owes plus the two DEFER lines the seat post carries — paid for by compressing in place, landing back at exactly 243/243, headroom 0. No ceiling raise, so no maintainer ruling is needed.

What paid: the maintainer quote duplicated in the emergency-triage section (preserved verbatim in check-half-states.mjs's H13 docblock — provenance is one line, per the ratchet's own remedy text), one narrative aside about a separate line of work, and re-wrapping. I verified with a word-level diff (git diff --word-diff-regex='[^[:space:]]') that no operative rule was dropped — only provenance prose and whitespace.

Runbook lines

  1. Post-kill recovery reads three states, not two — remote / container-disk-only / gone. Agents share one container, so a killed agent's worktree outlives the process; the middle state is the most valuable and nothing looked for it. A PM checking only the remote confidently tells the next dev "you are starting from scratch" on a false premise. Found work is handed on flagged UNVERIFIED — neither discarded nor trusted, because the surviving edits in the measured incident contained two defects, one a comment asserting a measurement that had never been run.
  2. Write order — flip the label only after the claim comment succeeds; the two writes are not atomic and a 502 between them manufactures a genuine H2 (three measured instances in one round). Reversed order carries a retry obligation, and a failed retry rolls the label back.

Folded DEFER lines: read rate_limit once before dispatch, and pnpm 10.31 rejects install --workspace-concurrency (confirmed live here — this worktree installed on pnpm v10.31.0).

Out of scope

No changeset: this PR ships no user-visible behaviour — it changes internal PM patrol tooling and a .claude/ instruction face only. skip-changeset applied.

Generated by Claude Code


Generated by Claude Code

A dev agent killed mid-task leaves a TEXTBOOK-correct claim: `pm:dispatched`,
assignee set, a first-line `Claim:` comment, and a branch that exists on the
remote. Every predicate in the patrol passed, so the card read as healthy
in-flight work indefinitely — and worse than merely unreported: the next PM's
round-open mutual-exclusion read treats a dead `Claim:` as a live claim by
another session and stays off the card, so one dead agent blocks the lane.
H20 is the near neighbour and misses it BY CONSTRUCTION, not by oversight:
os-dev.md makes pushing the empty branch the FIRST action of the task, so a
protocol-compliant agent that dies still leaves a ref and lands outside H20's
no-ref-at-all population. The better the dev follows the protocol, the more
invisible its death.
H27 keys on the conjunction the filing card asked for: `pm:dispatched` ∧ a
claim older than the protocol's OWN ~24h stale line ∧ a claimed branch that
EXISTS ∧ that branch has not moved since the claim ∧ no PR delivers the card.
The threshold is quoted from SKILL.md's 死认领回收 rule rather than measured —
the fleet has 2 liveness samples, which is not a distribution.
Costs no request: H27's 24h population is a strict subset of H20's 60-minute
one, and the shared branch probe now reads `/branches/<b>` instead of
`/git/ref/heads/<b>` — the same underlying ref, so H20's three states are
untouched, but the head commit DATE rides in on a payload the sweep had
already paid for.
Report-only and pointedly NOT a reclaim: the protocol's reclaim rule applies to
a branch that does not exist and states 「有带提交活分支的认领永不回收」, so
this row prescribes the three-state recovery inspection and never an assignee
drop. It deliberately under-reports the dev that pushed once and then died.
Self-test 997 -> 1058 cases, both directions pinned per term.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ
…te order (#11248)
Two runbook lines the dead-claim patrol row needs a reader to have, plus the two
DEFER lines the seat post carries — all four paid for by compressing in place, so
the file lands back on its 243-line ceiling with no ratchet raise.
1. Post-kill recovery reads THREE states, not two. `git ls-remote` answers only
"did it reach the remote"; agents in this fleet share one container, so a
killed agent's worktree outlives the process and its uncommitted edits are
usually still on disk. The middle state is the most valuable one and nothing
looked for it: a PM that checks only the remote confidently tells the next dev
"you are starting from scratch" on a false premise (measured — three files of
real edits nearly discarded). Found work is handed on flagged UNVERIFIED —
neither discarded nor trusted, because the surviving edits contained two
defects, one of them a comment asserting a measurement never run.
2. Write order: flip the label only AFTER the claim comment succeeds. The two
writes are not atomic, and a 502 on the comment with the label already flipped
manufactures a genuine H2 (three measured instances in one round). Reversed
order carries a retry obligation, and a failed retry rolls the label back.
Folded DEFER lines: read `rate_limit` once before dispatch (the quota is a
fleet-wide single point of failure — one limit took out three devs at once), and
pnpm 10.31 rejects `install --workspace-concurrency` (the flag is only legal on
script-running commands).
Compression paid: the duplicated maintainer quote in the emergency-triage section
(preserved verbatim in check-half-states.mjs's H13 docblock), one narrative aside
about a separate line of work, and re-wrapping. No operative rule was dropped —
verified with a word-level diff.
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
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation labels Aug 23, 2026
@os-zhuang
os-zhuang self-requested a review August 23, 2026 10:07
@hotlong
hotlong marked this pull request as ready for review August 23, 2026 11:37
@hotlong
hotlong added this pull request to the merge queueAug 23, 2026
Merged via the queue into main with commit 7233907Aug 23, 2026
33 checks passed
@hotlong
hotlong deleted the claude/issue-11248-dead-claim-predicate branch August 23, 2026 11:50
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@hotlong@claude