Skip to content

Closing-target claim guard reads a yielded claim as the newest claim and reds the winning branch #25791

Closed

Description

@objectstack-fleet

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 line already claimed — yielding), exactly as execution-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-ratchet and check:pm-skill-id-lint stay 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

  1. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    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. finding comes off.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-26T08:27Z
    Session: session_01LqKsGyMNFwwnbF5FTAqyPn (seat; implementation by an in-session os-dev subagent)
    Account: os-warren (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/project-thread-1r3slx (the seat thread's harness push branch — the measured platform exception to claude/issue-N-slug; this container pushes that branch only)
    Worktree: objectstack-issue-25791 in the seat's clone
    Domain: domain:skills
    Seat: domain:skills#2 (seat post #19287)
    File surface: scripts/check-closing-target-claim.mjs and its self-test; the claim reader in scripts/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; a references/** face takes the default tier), effort: high. ⚠️ Deviation from mode:thread for 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, one CONTRACT_REVIEW_TIER review of record before the queue.
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the risk is produced by the seat-lane claim reader (h46ClaimNamesBranch counts every Claim: including a yielded one; claimGovernance judges the newest claim-shaped comment including a yielded one), consumed by the guard scripts/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 #25780
    Thread-read: 6397361984
    Premise: holds — measured on origin/main 05f7758f: the red on PR #25780 needs BOTH halves. CLAIM_BRANCH_SHAPE parses only claude/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, so malformed stayed 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:dispatched cards' claims name neither guard file, and none of the 20 open PRs touches the three files (control: 7 of them touch scripts/)


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  4. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #26016 comment 6412666664 · domain:skills#2 · 2026-10-26T09:29Z


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:skills#2 · session_01LqKsGyMNFwwnbF5FTAqyPn · 2026-10-26T10:22Z

    probe reading
    PR #26016 merged: true, merge commit 003104e3
    origin/main git log origin/main --grep=26016 answers 003104e3; yieldedClaimIndexes present in both changed scripts
    merge queue no gh-readonly-queue/main/* head names #26016

    State: the card closed as completed through Fixes #25791; pm:dispatched removed in this act (the domain, area and type labels stay). The body of #26016 closes no other card.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:skillspriority:p2Medium: important, M3tooling

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions