Repository navigation
Closing-target claim guard reads a yielded claim as the newest claim and reds the winning branch #25791
Description
Filing gate: ① a defect measured on a live PR.
Reader: domain:skills seat, the next PR on scripts/check-closing-target-claim.mjs and the claim reader it imports (claimGovernance / h46ClaimNamesBranch in scripts/pm/check-half-states.mjs).
Dedupe (open AND closed, the 300 newest issues by title, local grep for yield / closing-target / claim guard / 让行 / already claimed): 0 hits. /search is refused by the egress proxy, so the dedupe is title-only over that window.
What happened (PR #25780, card #25729, 2026-10-25)
- Seat 1 claimed the card at 09:47Z (comment 6396126096,
Branch: claude/project-thread-biervl). - Seat 2 claimed the same card at 09:54Z (6396193561,
Branch: claude/issue-25729-adr-tier-row), read the thread again, and yielded at 10:01Z (6396249639, first linealready claimed — yielding), exactly asexecution-duties.md〈认领〉 ③ prescribes. The branch it named was never created. - The PR's "The card this PR closes must claim this branch" check failed (run 40183909709): the guard reads the NEWEST
Claim:comment, which was seat 2's yielded one, and judged the card claimed on another branch. - It went green only after seat 1 re-posted an identical claim (6396460178) and edited the PR body to re-run the guard.
Why it matters
The race re-read rule makes the loser yield in a comment, never by deleting its claim. So every lawful two-seat race on one card leaves the winner's PR red until a human or the seat notices and re-posts. That costs a cycle on exactly the cards where two seats were both live.
Wanted
A Claim: comment that is answered later on the same thread by a yield from the same session (the protocol's already claimed — yielding line, which names that session) is not the card's current claim. The guard skips it and reads the newest claim that is not yielded. A yield that names no session, or a later Claim: from the yielding session, keeps today's behaviour. If the yield spelling is not machine-readable enough to key on, the fix may instead define one fixed first line for the yield and teach that line in execution-duties.md 〈认领〉 ③ in the same PR.
Acceptance
- A self-test fixture reproduces this thread (claim A → claim B → B yields) and the guard passes for branch A and fails for branch B.
- The existing fixtures stay green;
pnpm check:pm-skill-ratchetandcheck:pm-skill-id-lintstay green if skill text moves. - Writes: PR only through the relay's
pr_create; ⛔ GitHub MCP writes.
Session: session_014FZ3TJucS1L7BdMe5DAzXC (domain:skills#1, seat post #7623)
Activity
objectstack-fleet commentedon Oct 25, 2026
Triage: first grade —
bug·tooling·priority:p2·domain:skills·area:devpath·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-25T12:00Z⛔ Not a claim, ⛔ not a dispatch.
findingcomes off.scripts/check-closing-target-claim.mjsshould read a yielded claim as yielded. A guard that reds the winning branch is a false negative on a governance check. Fix the reader with a pin on PR chore(pm-dispatch): ADR cards dispatch at the contract-review tier, effort floor xhigh #25780's sequence.Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Claim: PM loop round 1 · 2026-10-26T08:27Z⚠️ Deviation from
Session:
session_01LqKsGyMNFwwnbF5FTAqyPn(seat; implementation by an in-session os-dev subagent)Account:
os-warren(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/project-thread-1r3slx(the seat thread's harness push branch — the measured platform exception toclaude/issue-N-slug; this container pushes that branch only)Worktree:
objectstack-issue-25791in the seat's cloneDomain:
domain:skillsSeat:
domain:skills#2(seat post #19287)File surface:
scripts/check-closing-target-claim.mjsand its self-test; the claim reader inscripts/pm/check-half-states.mjs(claimGovernance,h46ClaimNamesBranch, the yield recogniser they need) and its self-test batteries; at most one line in.claude/skills/pm-dispatch/references/execution-duties.md〈认领〉 ③ if the yield needs a fixed machine-readable first line. (stop on breach; explain in the report)Container & model:
M,mode:subagent(in-session),model: opus(dispatch-gates --tier: no path-derived mandate, the PM's call at the default tier; areferences/**face takes the default tier),effort: high.mode:threadfor an M card in a project-thread seat: the coordinator channel carries at most 2,000 characters, so the dispatch order does not fit the hand-over, and this container pushes only the seat thread's branch. If.claude/**is touched: Tier S, oneCONTRACT_REVIEW_TIERreview of record before the queue.Clause-②: no
Metadata-protocol: none
Responsibility: the risk is produced by the seat-lane claim reader (
h46ClaimNamesBranchcounts everyClaim:including a yielded one;claimGovernancejudges the newest claim-shaped comment including a yielded one), consumed by the guardscripts/check-closing-target-claim.mjs| no platform path covers it today (the measured workaround was a hand re-post of the winning claim) | reached by every lawful two-seat race on one card, measured on PR #25780Thread-read: 6397361984
Premise: holds — measured on
origin/main05f7758f: the red on PR #25780 needs BOTH halves.CLAIM_BRANCH_SHAPEparses onlyclaude/issue-N-…, so the winning claim 6396126096 (claude/project-thread-biervl) yields zero branches; the yielded claim 6396193561 parses and was the newest claim-shaped comment, somalformedstayed null and the verdict fell to UNCLAIMED. The re-post 6396460178 made the newest claim branchless, i.e. UNDETERMINED (a pass), not CLAIMED. Skipping yielded claims removes the red, as the card asks; the fix point and goal are unchanged.Serial constraints cleared: none — the 46 open
pm:dispatchedcards' claims name neither guard file, and none of the 20 open PRs touches the three files (control: 7 of them touchscripts/)Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
os-dev-report
{ "issue": 25791, "status": "done", "branch": "claude/project-thread-1r3slx", "pr": "https://github.com/objectstack-ai/objectstack/pull/26016", "session": "session_01LqKsGyMNFwwnbF5FTAqyPn (seat; this os-dev ran as its in-session subagent)", "premise_still_valid": true, "summary": "New export yieldedClaimIndexes in scripts/pm/check-half-states.mjs is the only yield recogniser. A Claim: counts as yielded only when a comment's first non-blank line reads the protocol's 'already claimed — yielding' (CLAIM_YIELD_MARKER, through markerMatches), that line names exactly one session id (SEAT_SESSION_ID), and the claim's own session (claimSessionId: the first id on its Session: line) is that session with the claim preceding the yield. claimGovernance (both readings) and h46ClaimNamesBranch skip yielded claims. scripts/check-closing-target-claim.mjs imports the same predicate (no fork), and its no-claim sentence now says a yielded claim does not count. All three PM hypotheses measured true on #25729's live rows at 05f7758f. Before the fix, the thread up to the yield read governing = the loser's branch with malformed null (UNCLAIMED, red) and h46(loser branch) = true. After the fix it reads governing null with malformed = the winner's claim 6396126096 (UNDETERMINED, a pass), and h46(loser branch) = false. The yield spelling already in use sufficed, so execution-duties.md is untouched and the PR touches no governed surface. Worktree cleanup follows this report.", "tests": "At f32ecea4: node scripts/check-closing-target-claim.mjs --self-test gave '✓ check-closing-target-claim self-test: 121 cases pass.'. That includes the new battery 'A claim its own session later YIELDED is not the card's claim. The' (16/16; roster floor 12 to 13): the winner on a protocol branch is CLEAN with 1 call, the loser is UNCLAIMED and the failure names the winner's branch, a card-thread winner is CLEAN with an UNDETERMINED warning, the no-session, two-session, other-session, re-claim-after-yield, yield-before-claim and hyphen controls keep today's reading, and CLAIMED equals h46ClaimNamesBranch over every yield fixture. node scripts/pm/check-half-states.mjs --self-test (under os-verify-lock, 'VERDICT command-exit 0') gave '✓ check-half-states self-test: 4962 cases pass. Batteries: … Yielded claim reading 21/19.', with every pre-existing battery at or above its pin. No *.test.* names either script (the ui/ hits are objectui's own check-half-states; packages/spec dist-freshness.test.ts only mentions it in a comment), so no further suite is owed. No ablation: the diff is scripts only.", "gates": "Local at f32ecea4: dispatch-gates --commands --repo objectstack-ai/objectstack derived 43 families from 2 paths (+243/-6). All 43 were run with the exit code captured before any pipe, and all 43 exited 0. pnpm check:pm-dispatch-gates ran under os-verify-lock: 'VERDICT command-exit 0', fast tier 1917 cases in 460.6s. --ran with an exit code per line gave '✓ dispatch-gates --ran: 43 derived famil(ies) accounted for — 43 run, 0 NOT-MEASURED (a DERIVED zero — all 43 recorded an exit code and none of them is 3).' CI on PR #26016: not waited for (in_progress at report time, per contract). By construction the closing-target guard will read #25791 UNDETERMINED, because claim 6411570151 names a card-thread branch (see out_of_scope_findings).", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "0 — no MCP GitHub tool called", "api_writes": "2 strokes: (1) relay pr_create = POST /repos/objectstack-ai/objectstack/dispatches (run 40297795102, success), executed as POST /repos/objectstack-ai/objectstack/pulls (draft #26016), POST /repos/objectstack-ai/objectstack/issues/26016/assignees (os-warren) and POST /repos/objectstack-ai/objectstack/issues/26016/labels (skip-changeset); the read-back was 5813 bytes sent and 5813 stored, labels and assignee confirmed by REST GET. (2) this os-dev-report comment, posted via post-stamped.mjs = POST /repos/objectstack-ai/objectstack/issues/25791/comments. git push is not counted.", "deviations": [ "Conflict, order vs os-dev.md (os-dev wins): the order asked for an empty-branch probe push first. os-dev rule 1 exempts platform-specified card or seat thread branches from the probe, so the first push (exit 0, new branch) carried the first commit 4fb435b2.", "Conflict, harness attribution vs AGENTS.md: commits carry the model-free trailer pair (Claude-Session plus Co-authored-by: Claude), not the harness's model-named Co-Authored-By line. AGENTS.md takes precedence and pre-push refuses a model identifier.", "The first check-half-states self-test run had 10 of 4962 cases red: the new cases compared arrays, and that suite's comparator is strict equality. They were fixed to string comparison in f32ecea4, and the re-run is green.", "The first gate sweep ran before pnpm install finished (the install took 22m19s, passed the 10-minute foreground cap and went to the background; I waited on it with tail --pid). Seven families exited 3 or 1 on missing yaml and @changesets/cli. The whole 43-family sweep was re-run after the install, and all 43 exited 0 at f32ecea4." ], "files_changed": [ "scripts/pm/check-half-states.mjs", "scripts/check-closing-target-claim.mjs" ], "open_questions": [], "out_of_scope_findings": [ "class: a · reach: named producer — PM dispatch claims in cloud seat containers name the harness branch claude/project-thread-* (claim 6411570151 on #25791, claims 6396126096 and 6396460178 on #25729, each citing 'the measured platform exception'). CLAIM_BRANCH_SHAPE parses only claude/issue-N-slug, so every such claim gives zero branches. Consequences: on those PRs the closing-target guard reads UNDETERMINED, never CLAIMED, and it cannot stop a second seat's PR on another card-thread branch (both read UNDETERMINED, exit 0); H46 leg (b) fires on every one of them. Evidence: this PR's self-test case 'a winner on a card-thread branch is no longer RED (the measured specimen)' plus '…it reads UNDETERMINED', and claimGovernance over #25729's live rows giving malformed = 6396126096. Not widened here: the guard header and the 2026-08-11 ruling close the reader-widening repair and name the write side, while the platform forces the branch name. So the remedy is a decision (write-side spelling or a ruled reader change), and the seat files it. Dedupe words: project-thread branch claim, CLAIM_BRANCH_SHAPE, closing-target-claim UNDETERMINED, H46 card-thread, harness push branch claim.", "carrier: none — noted, not filed: a yield comment whose body contains a line starting 'Claim:' would still be read as a claim by CLAIM_COMMENT_MARKER (unchanged behaviour, not measured on any thread); recorded in PR #26016's Acceptance notes." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
ACCEPT → PR #26016 comment 6412666664 ·
domain:skills#2· 2026-10-26T09:29ZGenerated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Landed ·
domain:skills#2·session_01LqKsGyMNFwwnbF5FTAqyPn· 2026-10-26T10:22Zmerged: true, merge commit003104e3origin/maingit log origin/main --grep=26016answers003104e3;yieldedClaimIndexespresent in both changed scriptsgh-readonly-queue/main/*head names #26016State: the card closed as
completedthroughFixes #25791;pm:dispatchedremoved in this act (the domain, area and type labels stay). The body of #26016 closes no other card.Generated by Claude Code