fix(coord): a pruned worktree stranded its work claims forever (BACKLOG #345) - #141
Merged
Merged
Conversation
#345) A claim (scripts/coord/claim.ps1) is a JSON file under <git-common-dir>/mefor-coord/claims/. It lives beside the SHARED object store, so it outlives the worktree that took it. prune-merged.ps1 removed worktrees and, across 1,108 lines, never touched claims -- and `claim.ps1 -Take` hard-blocks on any claim file that exists. So every prune could leave a key unclaimable by every future session until a human happened to run `-Release <key> -Force`. Nothing surfaced the condition and nothing could: the registry has no way to observe that a holder ceased to exist. Released on EVIDENCE, never on a timer. claim.ps1 is right that an auto-expiring claim silently re-opens the race it exists to prevent; this hooks only into the branch that has already PROVEN the directory is gone and deregistered ("OUTCOME, NOT EXIT CODE"), so there is no session left to collide with. A merely-quiet holder is never touched. Dry-run safety is inherited, not re-asserted: the whole apply loop is gated on -Apply. The false positive is worse than the bug. Releasing a claim held by a different, LIVING worktree hands its key away and invites the duplicate build the registry exists to stop, so the match is full normalised equality via ConvertTo-Norm -- no leaf name, no prefix, no StartsWith. Two tests pin it: a living sibling's claim survives, and the PRIMARY checkout's claim survives a sibling prune even though `<primary>` is a strict prefix of `<primary>-clean`. Both carry a positive control in the same invocation, so neither can pass on a run that released nothing. Two defects found by the tests rather than by review, both of the shape this file already guards against elsewhere: * an unreadable claim was counted once per REMOVED worktree, so one blocked key reported as 2. It belongs to the registry, not to any worktree -- not being able to read it is precisely not knowing whose it is -- so it is now surveyed once per run and reported at run level. * that survey ran only inside the removal branch, making it invisible to a dry run, and its Set-Exit sat AFTER the -Json block that emits the receipt and exits. The receipt would have carried exitCode 0 over a key nothing can claim -- the surface CI reads. `claims.scanned` distinguishes "read the registry, found it clean" from "never looked" (no git-common-dir, or no claims directory): an empty `unreadable` list is not a green light. Verified: the 8 new tests FAIL against the unpatched script before they pass against the patched one, so they can see the class. Full file green, 71 tests.
wshallwshall
enabled auto-merge (squash)
August 2, 2026 04:38
That is the exact defect the backlog-hygiene gate exists for -- #60 shipped while its banner still read "PRE-RESERVED", and the stale banner was then repeated as fact in a merged PR. The gate did not fire here because it only inspects engine/IDE paths and this item is script-and-test work, which is precisely why the banner had to be corrected by hand rather than trusted to CI. Records what actually shipped (Half A) against what did not (Half B), and why Half B was backed out: claim.ps1 had three sessions in it at once, so the deferral is a coordination decision, not an estimate of difficulty. Someone reading this item needs that distinction to know it is pickup-able. The first draft of this named the contending BRANCH and the leak gate blocked it -- a worktree slug is internal naming and this ledger is public. Described by the code path it touches instead.
…described it WORKTREES.md is where the pruner's outcome contract is written down -- exit codes, which counts are subsets of which, why `git worktree prune` is never run. A new field in that receipt that only the source explains is the same drift this section already guards against elsewhere. States the property that makes the release safe rather than just the behaviour: it fires on PROVEN deletion, so a quiet holder is untouched and a dry run releases nothing, and the match is full path equality because freeing a LIVING worktree's key is worse than the orphan being cleaned up.
…-less script prune-merged.ps1 had exactly ZERO non-ASCII bytes before this branch and no BOM -- every comment in it uses the ASCII `--`. A BOM-less file whose encoding is only inferred is a bad place to start introducing multi-byte characters: pwsh 7 assumes UTF-8 and would be fine, Windows PowerShell 5.1 assumes the ANSI codepage and would not, and nothing in the repo pins which one an operator runs it with. The file is back to 0 non-ASCII bytes. Comments only; CRLF line endings verified byte-identical in count before and after, and the claim tests re-run green.
#136 landed four BACKLOG items (#339, #341, #342, #343) at the tail of the same file #345 appends to, so git saw two additions at one anchor and called the whole region a conflict. There is no semantic conflict: different items, no shared number, no shared prose. Resolved by WORKTREES.md's recipe for this shape -- take main's BACKLOG.md wholesale, then re-insert only this branch's added item. Taking main whole is what stops a hand-merge from silently reverting somebody else's landed work, which is the real hazard here; a lost #345 would be obvious, a lost #341 would not. Verified rather than eyeballed: 268 item headings on main -> 269 after, NONE lost, exactly {345} added, no duplicated numbers, no conflict markers, and the status invariant still scans clean. Ordering follows main's own convention -- items are appended in landing order, not sorted, and 345 is not present anywhere on main.
wshallwshall
added a commit
that referenced
this pull request
Aug 2, 2026
PR #141 landed BACKLOG #345 at the file's EOF, the same append point as #347. Resolved by keeping BOTH items, main's #345 first (it is already on main) then #347, which is what the sandbox-codec session proposed when we compared append points: two independent OPEN items at EOF carry no ordering meaning. Merged rather than rebased deliberately. Each of the five #347 commits rewrites the same block, so a rebase re-raises the same conflict five times and risks a mid-stack resolution silently keeping an earlier draft; a merge resolves the seam once against the FINAL text. The branch squashes on land, so the merge commit does not reach main. Verified rather than assumed: backlog_status_check.py reports 270 items each with exactly one banner -- the count that session predicted for both items landing -- and #347's post-merge text still carries all three of its late revisions (the infinitely-fast-machine discriminator, the ADR 0158 rule citation, the float64 underflow callout), so the resolution kept the final block and not an early draft.
wshallwshall
added a commit
that referenced
this pull request
Aug 2, 2026
#141 (BACKLOG #345) landed while #148 was in CI. Both PRs append a new item at EOF after #343, so this conflicted on exactly one hunk - the predicted collision, not a surprise. gh pr update-branch CANNOT resolve it; it fails and leaves the PR DIRTY. Resolution: TAKE BOTH. #346 and #345 are independent OPEN items appended at the same point, so their relative order carries no meaning (this file has not been numerically ordered since #340 - the tail reads 340, 344, 339, 341, 342, 343). Restored the `---` separator the conflict markers had displaced. Verified by measurement rather than by eyeballing whether the markers were gone: - conflict markers remaining: 0 - backlog_status_check.py: 270 items, each with exactly one status banner (269 before + #345 - the count I predicted BEFORE resolving, which is what makes it a check rather than a description) - `git diff origin/main -- docs/BACKLOG.md` = exactly 3 hunks, all mine: #197's banner, #339's OPEN item 2, and #346 appended - #345's block diffed byte-identical against origin/main - nothing of #141's lost - CRLF preserved, 0 bare LF Coordination: the resolving edit was blocked by the collision gate while the #347 session held this file dirty mid-rebase. Waited for it to commit rather than overriding the gate or rewriting the file with a shell command - routing around a guard through a tool it does not hook is overriding it with extra steps.
wshallwshall
added a commit
that referenced
this pull request
Aug 4, 2026
…aid 93 Caught in review. I moved three of the four lines, not four: "Every one of the **92 open items** is re-scored here" (:144) still read 92 while Tiers and "sum to N" read 93. Line 1 disagreed with lines 2-4 on the branch as pushed, which is precisely what the "all four lines sum to N" invariant exists to catch -- and my recompute script only rewrote the three lines it generated, never the prose sentence. Verified this tree with the repo's OWN parser rather than my regex: parse_items -> 93 items, 93 open, 0 closed. My whole-range banner scan agreed exactly. That agreement is luck, not method, and worth recording as such. parse_items ends an item's banner block at the first line that is neither blank nor a blockquote, so a `✅` sitting AFTER an item's prose is batch-filing narrative, not that item's status. My scan read the whole heading range and would have swallowed one. No item in this file currently has that shape, so the two methods agree here and would diverge on a file that does -- a reviewer's hand-rolled checker got three different wrong answers today, including calling #3, #105 and #141 closed. Use parse_items. NOT PREDICTING THE POST-#177 NUMBER. Once #177 lands (it closes #335), this tree becomes 93 items / 92 open / 93 rows, so the census wants 92. I am deliberately NOT writing 92 now: predicting it is carrying a delta in my head, which is the thing re-derive-never-delta forbids. All four lines are set to 93, which is derived and correct for THIS tree, and the number gets re-derived from the merged tree afterwards. Worth recording because it is the rule catching its own author: the two defects PARTIALLY CANCEL. Post-merge the line I missed (92) would have become accidentally correct while the 93s I did update became wrong -- and a total-only assertion sails straight through that. It is the same cancellation failure the RULE 1 amendment on this very branch warns about, arriving in the PR that carries the warning.
wshallwshall
added a commit
that referenced
this pull request
Aug 4, 2026
…ockers, amend §D RULE 1 (#178) * docs: file BACKLOG #1003 (lab validation), amend §D RULE 1 by owner ruling Three changes, batched into one PR because the queue is rate-limited by PR count, not by diff size. 1. BACKLOG #1003 — validate the lab and discharge the four hardware-gated residuals. A multi-VM server is ~2 weeks out. Four open items are blocked on that one missing thing: #99 (the live domain-lab gMSA/SSO smoke — its ONLY remaining residual), #98 (Kerberos EPA, same DC + AD CS gate), #320 (the decisive windows-2025 sweep, blocked on an unregistered runner) and #351 (execute the failover patch against a real SQL Server, and measure ADR 0159's _acquire cost). TRIGGER IS "LAB AVAILABLE FOR VALIDATION", NOT "LAB VALIDATED" -- owner's correction to my wording, and it was circular: proving the lab does what these items need is this item's own first deliverable, so gating on validation means the trigger can never fire. Not a roadmap umbrella. Its deliverables are runs with recorded outcomes, which is the distinction that scored #64 a 1/10 for shipping nothing runnable. #351's measurement is called out as the one that is easy to lose: its patch makes the test deterministic, which removes the only thing currently raising the latency question. The item itself says a lease-election test is the wrong instrument for discovering latency -- so the measurement belongs on the rig, and landing the patch without it drops the question rather than answering it. 2. #99, #98, #320 and #351 each get a dated note that the hardware blocker HAS AN EXPIRY. #99 currently says its residual is "rig/provisioning the project does not own"; #320 says its runner is unregistered. Both are true today and scheduled to become false. Left alone those sentences keep telling every planning pass the work is unreachable -- the same stale-premise rot the 2026-07-28 reconcile found on five items and the 2026-08-03 re-score found on twenty-four. 3. §D RULE 1 amended BY OWNER RULING: the ranked table and census are not owner-only, they are recomputed by whoever writes the ledger last. Recorded as an owner decision rather than a correction, because that is exactly the standing an earlier branch of mine lacked -- it reversed the same clause on an inter-session agreement in no committed document, and was rightly refused. The argument did not change; the authority did. Recorded with the condition that makes it safe: it depends on the unit being open-bannered HEADINGS and on the re-derivation being one operation with the stale-banner sweep. Without both, sessions recomputing is worse than the stale rule. Owner-only was never safer on correctness -- it leaves the census stale whenever the owner is away, and an owner re-deriving from stale banners launders the same false-open. CENSUS RE-DERIVED under the corrected rule, and the two-directional heading<->row comparison run rather than a total-only check: 93 open headings, 93 rows, zero headings without a row, zero rows without an open heading. All four lines sum to 93. Tiers: P1 5, P2 19, P3 17, DEMAND-GATE 52 Quadrants: quick win 24, big bet 5, fill-in 55, money pit 9 The bijection check is the point, not the total: an over-count and an under-count cancel, so a total that adds up is not evidence the table and the items agree. Number allocated atomically via alloc.ps1 (#1003; #1002 was already taken). Never grepped. * docs(backlog): the fourth census line said 92 while the other three said 93 Caught in review. I moved three of the four lines, not four: "Every one of the **92 open items** is re-scored here" (:144) still read 92 while Tiers and "sum to N" read 93. Line 1 disagreed with lines 2-4 on the branch as pushed, which is precisely what the "all four lines sum to N" invariant exists to catch -- and my recompute script only rewrote the three lines it generated, never the prose sentence. Verified this tree with the repo's OWN parser rather than my regex: parse_items -> 93 items, 93 open, 0 closed. My whole-range banner scan agreed exactly. That agreement is luck, not method, and worth recording as such. parse_items ends an item's banner block at the first line that is neither blank nor a blockquote, so a `✅` sitting AFTER an item's prose is batch-filing narrative, not that item's status. My scan read the whole heading range and would have swallowed one. No item in this file currently has that shape, so the two methods agree here and would diverge on a file that does -- a reviewer's hand-rolled checker got three different wrong answers today, including calling #3, #105 and #141 closed. Use parse_items. NOT PREDICTING THE POST-#177 NUMBER. Once #177 lands (it closes #335), this tree becomes 93 items / 92 open / 93 rows, so the census wants 92. I am deliberately NOT writing 92 now: predicting it is carrying a delta in my head, which is the thing re-derive-never-delta forbids. All four lines are set to 93, which is derived and correct for THIS tree, and the number gets re-derived from the merged tree afterwards. Worth recording because it is the rule catching its own author: the two defects PARTIALLY CANCEL. Post-merge the line I missed (92) would have become accidentally correct while the 93s I did update became wrong -- and a total-only assertion sails straight through that. It is the same cancellation failure the RULE 1 amendment on this very branch warns about, arriving in the PR that carries the warning.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A work claim (
scripts/coord/claim.ps1) is a JSON file under<git-common-dir>/mefor-coord/claims/. It lives beside the shared object store, so it outlives the worktree that took it.prune-merged.ps1removes worktrees and — across 1,108 lines — never touched claims. Sinceclaim.ps1 -Takehard-blocks on any claim file that exists, every prune could leave a key unclaimable by every future session until a human happened to run-Release <key> -Force.Nothing surfaced the condition and nothing could: the registry has no way to observe that a holder ceased to exist. That is the "a control that cannot see its own failure" class named alongside #344.
Scope — this is Half A only
prune-merged.ps1releases claims of a worktree it has proven gone-Take/-Releasesurface holder liveness-Listliveness landed separately in #106. Half B (the blocking paths, which is the half an operator actually hits) is deliberately left out:claim.ps1is currently contended by #106 and by a live session's note-refresh work, and a three-way edit of one 170-line script is how merge messes start. It is recorded in the #345 item body.Why this is not an expiring claim
claim.ps1's docs are right that an auto-expiring claim silently re-opens the race it exists to prevent. This releases on evidence, not elapsed time: it hooks only into the branch that has already proven the directory is gone and deregistered ("OUTCOME, NOT EXIT CODE"), so no session can still be in there. A merely-quiet holder is never touched. Dry-run safety is inherited rather than re-asserted — the whole apply loop is gated on-Apply.The false positive is worse than the bug
Releasing a claim held by a different, living worktree hands its key away and invites the duplicate build the registry exists to stop. So the match is full normalised equality via
ConvertTo-Norm— no leaf name, no prefix, noStartsWith. Two tests pin it, each with a positive control in the same invocation so neither can pass on a run that released nothing:<primary>is a strict prefix of<primary>-clean.Two defects the tests caught, not review
2. It belongs to the registry, not to any worktree — not being able to read it is precisely not knowing whose it is — so it is now surveyed once per run.Set-Exitsat after the-Jsonblock that emits the receipt and exits. The receipt would have carriedexitCode: 0over a key nothing can claim — the surface CI reads.claims.scanneddistinguishes "read the registry, found it clean" from "never looked" (no git-common-dir, or no claims directory). An emptyunreadablelist is not a green light.Verification
The 8 new tests were run against the unpatched script first: 6 of 7 then-written tests failed, including both negative tests via their positive controls. Full file green afterwards — 71 passed.
🤖 Generated with Claude Code