Skip to content

fix(coord): claim.ps1 read a directory as an occupant, so an abandoned worktree looked like a live lane (BACKLOG #1348) - #585

Merged
wshallwshall merged 7 commits into
mainfrom
claude/builder-1-1348-claim-liveness-v2
Aug 26, 2026
Merged

fix(coord): claim.ps1 read a directory as an occupant, so an abandoned worktree looked like a live lane (BACKLOG #1348)#585
wshallwshall merged 7 commits into
mainfrom
claude/builder-1-1348-claim-liveness-v2

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Get-HolderLiveness returned present when the holder's path EXISTED. It never asked whether a session was in it, so a worktree that outlived its session rendered identically to a lane actively building -- a third state the tool could not represent. Measured on the live board: 35 gone, 8 with a live session, 23 that were a directory with nobody in it -- and those 23 had been wearing the same label as the 8.

The new state REPORTS, it does not PERMIT: still refuses -Take, still does not recommend -Force. occupancy.ps1's rule is that it may only ever VETO, never authorise -- a session working the path by absolute path from another cwd is invisible to a cwd-keyed probe, so the second state stays non-releasable by design.

Mutation-proved: the fail-safe swap survived a first-occurrence text-match repair (both branches return plausible values), caught only by a before/after file hash; repaired by line range instead, binary-safe, byte-identical hash to pre-mutation.

Verification: 14 passed in tests/test_coord_claim_liveness.py (9 -> 14, extended the existing module), 79 passed across every other module that drives claim.ps1. ruff clean.

…d worktree looked like a live lane (BACKLOG #1348)

`Get-HolderLiveness` returned `present` when the holder's path EXISTED. It never asked whether a
session was in it, so a worktree that outlived its session rendered identically to a lane actively
building. That is a third state the tool could not represent, and it is the one that produces work
which is done, correct, and held by nobody: the sanctioned verbs refuse, CLAUDE.md forbids
releasing another worktree's claim, and nothing can regularise it.

MEASURED ON THE LIVE BOARD, before and after, same command:
    35  HOLDER GONE            unchanged
     8  LIVE SESSION in it     was indistinguishable from the next row
    23  DIRECTORY ONLY         previously rendered as "holder last committed Nh ago"
So 23 of the 31 non-gone holders were abandoned directories wearing the same label as the 8 real
lanes. The dispatcher's independent split put that middle band at 16; mine says 23. DIFFERENT
INSTRUMENTS, DIFFERENT NUMBERS, and I am not reconciling them by picking one -- theirs was taken at
a different time and may key liveness differently. The shape is what both agree on.

*** THE STATE REPORTS. IT DOES NOT PERMIT. *** `unoccupied` REFUSES exactly as `present` does, and
that is not timidity. occupancy.ps1 states the rule this inherits: "there is no heartbeat on this
host, so nothing here can prove a session is GONE. Occupancy may therefore only ever VETO an
action; a DEAD/STALE/absent verdict must never by itself authorise one." The item asked that the
two be DISTINGUISHABLE, not that the second become releasable. A cwd-keyed probe cannot see a
session working in that path BY ABSOLUTE PATH from elsewhere, and the deny text now says so.

POLARITY, the same rule the blanket-stage guard carries (#1341, and #1229 before it): recognition
may only ever SUPPRESS. Downgrading to `unoccupied` needs a POSITIVE determination -- the probe
loaded, reported itself Available, and returned zero vetoing sessions for that exact path. Every
other outcome, including occupancy.ps1 failing to load at all, stays `present` and refuses. A
missing answer costs a refusal, never a licence.

COST, measured rather than assumed, because -List runs constantly: Get-WorktreeOccupancy is 304 ms
once, Get-WorktreeOccupants 9 ms per lookup, and it is built LAZILY and cached for the invocation.
Full -List over 66 claims: 1.8 s wall.

TESTS -- extended tests/test_coord_claim_liveness.py rather than adding a module. IT ALREADY
EXISTED; I nearly wrote a duplicate off a filename in a dispatch note before reading the tests
directory. 9 -> 14 cases.
  the three states are distinguishable, and a vanished holder still outranks the new one
  the new state still REFUSES a -Take and does not recommend -Force on a -Release
  *** the fallback: with occupancy.ps1 absent, the probe cannot load and the holder must read as
      present and still refuse -- never as the new state. The base fixture copies only claim.ps1,
      so it produces that condition naturally, which is why the pre-existing tests were already
      exercising the fail-safe and passing.

MUTATION-PROVED, with a hash gate on both ends: flipped the fallback to return `unoccupied`,
confirmed by hash the mutant applied (609b0017 -> 10940148), scored 4 failures including the
polarity test and three pre-existing ones.

*** AND THE RESTORE WAS ITSELF A BAD MUTATION, CAUGHT ONLY BY THE HASH. *** Restoring searched for
the one-line `unoccupied` return and hit the FIRST occurrence -- the real third-state branch --
not the mutated fallback, silently swapping the two. The file was left functionally wrong: the
third state could never fire. The tests would not reliably have caught it, because the swap leaves
both branches returning plausible values. A RESTORE IS A MUTATION AND NEEDS THE SAME VERIFICATION
AS THE MUTANT. Repaired by line range, binary-safe so the file's 549 CRLF endings survived, and the
hash is byte-identical to pre-mutation.

VERIFIED, scope named:
  pytest 14 passed  tests/test_coord_claim_liveness.py
  pytest 79 passed  every other module that drives claim.ps1 (test_coord_claim_refresh,
    test_coord_claim_release_history, test_coord_claim_adjudicate, test_coord_claim_reconcile,
    test_claim_check, test_coord)
  ruff format --check and ruff check -- run separately, each with its own exit code, both clean
  the live board rendered three ways before and after the repair, same counts
  NOT a full-suite run

COORDINATION: the Cleaner is running a claim survey off this tool's output and I told them the
predicate before it landed, as they asked. Their census of "3 of 62 held by a live session" is the
figure this change makes legible; the label on 23 rows moves with no change to any claim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wshallwshall
wshallwshall enabled auto-merge (squash) August 25, 2026 18:15
wshallwshall and others added 6 commits August 25, 2026 14:17
… never observed (BACKLOG #1348)

CI FIX for PR 585. The required windows-2025 leg failed three of the third-state tests,
deterministically rather than as a flake, and it is a real bug in the feature -- not an environment
quirk.

*** THE BUG: ONE STATE MEANT TWO THINGS. *** `Get-HolderLiveness` returned `present` BOTH when the
probe looked and found an occupant AND when the probe could not look at all -- and then `present`
was labelled "LIVE SESSION in the holder". So wherever occupancy is unavailable the tool ASSERTED A
LIVE SESSION IT HAD NEVER OBSERVED.

That is the exact conflation this item exists to remove -- telling a DIRECTORY apart from a PERSON --
reintroduced one level down, in the deny text, by me.

REPRODUCED LOCALLY BEFORE FIXING, rather than reasoned about from the CI log:

    no Claude config root at all          Available=False  "no Claude config root ... was found"
    a config root, empty sessions dir     Available=False  "not one readable session record in them"
    a config root + one session record    Available=True   Sessions=0

A CI runner has no session registry, so the probe reports unavailable and the fallback fires.

THE FIX IS A STATE SPLIT, and `present` goes back to meaning exactly what it meant before #1348 --
the path is there and that is all this function knows:
    gone        path absent
    occupied    path + probe AVAILABLE + a vetoing occupant for this exact path
    unoccupied  path + probe AVAILABLE + zero occupants        <- the third state, unchanged
    present     path + probe UNAVAILABLE, occupancy UNKNOWN    <- no longer claims anything
    unknown / failed                                            unchanged
`occupied` is now the ONLY state that claims a session, and it is reachable only through a probe
that returned Available.

A REGRESSION MY OWN EDIT INTRODUCED AND THE EXISTING TESTS CAUGHT. My first split gave `present` a
shorter message that dropped the do-not-Force guidance the occupied branch carries.
`test_take_blocked_by_a_LIVING_holder_does_not_offer_force` and its release sibling failed on
exactly that. The two branches now differ ONLY in the liveness claim; the advice is identical, and a
test asserts that.

*** THE TEST FIXTURE WAS THE OTHER HALF OF THE DEFECT, AND IT IS THE WORSE SHAPE. *** The
third-state tests asserted DIRECTORY-ONLY, which is reachable only through an AVAILABLE probe, while
depending on the developer machine happening to have a Claude session registry. They passed locally
and failed on windows-2025: green exactly where nobody is watching. `repo_with_occupancy` now PLANTS
its own config root with one session record carrying a DEAD pid -- which makes the probe available
while placing no occupant anywhere, since occupancy is decided by whether the pid is running. The
measured table above is in the fixture docstring so the next reader does not have to re-derive why a
config root alone is not enough.

NEW TEST: `test_an_unavailable_probe_never_claims_a_live_session`, on the base fixture where the
probe genuinely cannot load. Asserts the output does NOT say "LIVE SESSION", DOES say "OCCUPANCY
UNKNOWN", and still refuses -- naming the unknown is not a licence.
MUTATION: reintroduce the bug (fallback returns `occupied`) -> that test alone fails. Restored from
a byte copy, hash-verified identical.

VERIFIED, scope named:
  pytest 32 passed  test_coord_claim_liveness (14 -> 15) + test_coord_claim_refresh
                    + test_coord_claim_release_history
  pytest 28 passed  test_coord_claim_adjudicate + test_coord
  ruff format --check, ruff check -- run separately, both clean
  the live board still separates three ways on this box
  NOT a full-suite run, and NOT verified on windows-2025 -- that leg is what found this and only CI
  can run it. The Lander should re-check there rather than take this commit as proof.

WHAT I CANNOT CLAIM: this is not corroborated by a second Windows leg. The module is
skipif(os.name != "nt") and the job has no windows-2022 leg, so windows-2025 remains the only data
point either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wshallwshall
wshallwshall merged commit 0849620 into main Aug 26, 2026
38 of 39 checks passed
@wshallwshall
wshallwshall deleted the claude/builder-1-1348-claim-liveness-v2 branch August 26, 2026 01:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant