fix(coord): a pruned worktree stranded its work claims forever (BACKLOG #345) - #141
Open
wshallwshall wants to merge 4 commits into
Open
fix(coord): a pruned worktree stranded its work claims forever (BACKLOG #345)#141wshallwshall wants to merge 4 commits into
wshallwshall wants to merge 4 commits into
Conversation
…OG #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.
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