Skip to content

Commit 0849620

Browse files
wshallwshallwshallwshallclaude
authored
fix(coord): claim.ps1 read a directory as an occupant, so an abandoned worktree looked like a live lane (BACKLOG #1348) (#585)
* fix(coord): claim.ps1 read a directory as an occupant, so an abandoned 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> * fix(coord): the unknown-occupancy fallback asserted a live session it 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> --------- Co-authored-by: wshallwshall <mefordev@messagefoundry.org> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent a69e830 commit 0849620

2 files changed

Lines changed: 303 additions & 8 deletions

File tree

scripts/coord/claim.ps1

Lines changed: 116 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ $ErrorActionPreference = "Stop"
6868
# Hook right, tool wrong, and only the tool can be invoked from somewhere else.
6969
$repo = (& git -C $PSScriptRoot rev-parse --path-format=absolute --show-toplevel 2>$null)
7070
if (-not $repo) { throw "scripts/coord/ is not inside a git repository: $PSScriptRoot" }
71+
72+
# Occupancy is what lets a holder that is a DIRECTORY be told apart from one that is a SESSION
73+
# (BACKLOG #1348). Loaded best-effort and NEVER fatal: if this fails, Get-HolderLiveness keeps its
74+
# older, stricter meaning and refuses, which is the safe direction. Dot-sourced at script scope
75+
# because functions sourced inside a function do not survive it.
76+
try { . "$PSScriptRoot/occupancy.ps1" } catch { }
7177
$repo = $repo.Trim()
7278
$common = (& git -C $repo rev-parse --path-format=absolute --git-common-dir).Trim()
7379
$claims = Join-Path $common "mefor-coord/claims"
@@ -154,23 +160,82 @@ function Add-HistoryLine([string]$Line) {
154160
# Reports only what it can PROVE. A vanished directory is a fact and the one state safe to act on
155161
# unasked. Everything else is 'unknown' or a quiet-hours count -- never "probably fine": a session can
156162
# be alive and simply not committing, so silence is not evidence of death.
163+
# A DIRECTORY IS NOT A PERSON, AND THIS FUNCTION USED TO CONFLATE THEM (BACKLOG #1348).
164+
#
165+
# `present` meant "the path exists". It never asked whether a SESSION was in it, so a worktree that
166+
# outlived its session -- the directory still on disk, nobody in it -- rendered identically to a
167+
# lane that is actively building. That is a THIRD state the tool could not represent, and it is the
168+
# one that produces work that is done, correct, and held by nobody: the sanctioned verbs refuse,
169+
# `-Force` is forbidden by CLAUDE.md, and the claim cannot be regularised by anything.
170+
#
171+
# ***THE NEW STATE REPORTS. IT DOES NOT PERMIT.*** `unoccupied` still REFUSES, exactly as `present`
172+
# does, and this is not timidity -- `occupancy.ps1` states the rule it inherits: "there is no
173+
# heartbeat on this host, so nothing here can prove a session is GONE. Occupancy may therefore only
174+
# ever VETO an action; a DEAD/STALE/absent verdict must never by itself authorise one." What the
175+
# item asked for is that the two be DISTINGUISHABLE, not that the second become releasable.
176+
#
177+
# POLARITY, and it is the same rule the blanket-stage guard carries (#1341, #1229): recognition may
178+
# only ever SUPPRESS. Downgrading to `unoccupied` requires a POSITIVE determination -- the occupancy
179+
# probe loaded, reported itself Available, and returned zero vetoing sessions for that exact path.
180+
# Every other outcome, including the probe failing to load at all, stays `present`. A missing
181+
# answer must cost a refusal, never a licence.
182+
$script:OccupancyProbe = $null # $null = not yet tried; $false = unavailable; else the object
183+
184+
function Get-OccupancyOnce {
185+
if ($null -ne $script:OccupancyProbe) { return $script:OccupancyProbe }
186+
try {
187+
if (-not (Get-Command Get-WorktreeOccupancy -EA SilentlyContinue)) {
188+
$script:OccupancyProbe = $false
189+
return $false
190+
}
191+
$occ = Get-WorktreeOccupancy -Repo $repo
192+
# `Available` false means the fence could not be built -- an unplaceable record, no repo.
193+
# Treat that exactly like a failed probe.
194+
$script:OccupancyProbe = if ($occ -and $occ.Available) { $occ } else { $false }
195+
}
196+
catch { $script:OccupancyProbe = $false }
197+
return $script:OccupancyProbe
198+
}
199+
157200
function Get-HolderLiveness([string]$HeldPath) {
158201
try {
159202
if (-not (Test-Path -LiteralPath $HeldPath)) {
160-
return [pscustomobject]@{ State = 'gone'; QuietHours = $null }
203+
return [pscustomobject]@{ State = 'gone'; QuietHours = $null; Occupants = $null }
161204
}
162205
$ct = & git -C $HeldPath log -1 --format=%ct 2>$null
163206
if ($ct) {
164207
$quiet = [int]((Get-Date) - [System.DateTimeOffset]::FromUnixTimeSeconds([long]$ct).LocalDateTime).TotalHours
165-
return [pscustomobject]@{ State = 'present'; QuietHours = $quiet }
208+
$occ = Get-OccupancyOnce
209+
if ($occ) {
210+
$who = @(Get-WorktreeOccupants -Occupancy $occ -Path $HeldPath)
211+
if ($who.Count -eq 0) {
212+
return [pscustomobject]@{ State = 'unoccupied'; QuietHours = $quiet; Occupants = 0 }
213+
}
214+
return [pscustomobject]@{ State = 'occupied'; QuietHours = $quiet; Occupants = $who.Count }
215+
}
216+
# PROBE UNAVAILABLE -> `present`, which keeps its PRE-#1348 meaning exactly: the path is
217+
# there and that is all this function knows. It still refuses.
218+
#
219+
# THIS SPLIT IS A BUG FIX, NOT A TIDY-UP. The first version of #1348 returned `present`
220+
# for BOTH "the probe looked and found an occupant" and "the probe could not look", and
221+
# then labelled `present` "LIVE SESSION in the holder". On a machine with no Claude
222+
# config root -- every CI runner -- the probe reports Available=false, so the fallback
223+
# fired and the tool ASSERTED A LIVE SESSION IT HAD NEVER OBSERVED. Caught by the
224+
# required windows-2025 leg on PR 585, three tests, deterministic rather than flaky.
225+
#
226+
# One state cannot mean both "I measured this" and "I could not measure this", and the
227+
# deny text is where that conflation becomes a false statement to an operator. `occupied`
228+
# is now the only state that claims a session, and it is reachable only through a probe
229+
# that returned Available with a vetoing occupant for this exact path.
230+
return [pscustomobject]@{ State = 'present'; QuietHours = $quiet; Occupants = $null }
166231
}
167232
# Present on disk but no commit to date it by -- a brand-new worktree looks exactly like this.
168-
return [pscustomobject]@{ State = 'unknown'; QuietHours = $null }
233+
return [pscustomobject]@{ State = 'unknown'; QuietHours = $null; Occupants = $null }
169234
}
170235
catch {
171236
# Say so rather than returning 'gone'. A failed probe that reported death would turn an
172237
# unreadable path into a licence to release someone's live claim.
173-
return [pscustomobject]@{ State = 'failed'; QuietHours = $null }
238+
return [pscustomobject]@{ State = 'failed'; QuietHours = $null; Occupants = $null }
174239
}
175240
}
176241

@@ -199,9 +264,22 @@ function Show-List {
199264
$live = Get-HolderLiveness $held
200265
switch ($live.State) {
201266
'gone' { $age = " [HOLDER GONE -- worktree no longer exists; release with -Force]" }
267+
'occupied' {
268+
$age = " [held ${hrs}h; LIVE SESSION in the holder, last committed $($live.QuietHours)h ago]"
269+
if ($live.QuietHours -ge 12) { $age += " -- QUIET but OCCUPIED, ask before releasing" }
270+
}
271+
# The probe could not run -- no Claude config root, or it failed. Says what it knows
272+
# and no more: the path is there. It must NOT claim a session it never looked for.
202273
'present' {
203-
$age = " [held ${hrs}h; holder last committed $($live.QuietHours)h ago]"
204-
if ($live.QuietHours -ge 12) { $age += " -- QUIET, confirm with the holder before releasing" }
274+
$age = " [held ${hrs}h; holder present, last committed $($live.QuietHours)h ago; OCCUPANCY UNKNOWN -- the session probe could not run]"
275+
}
276+
# THE THIRD STATE, and the listing is the surface that matters (BACKLOG #1348).
277+
# -List is what a Cleaner or Dispatcher reads to decide where to spend attention, so
278+
# a directory that outlived its session must not render identically to a lane that
279+
# is building. It still says ROUTE, not release: the refusal is unchanged.
280+
'unoccupied' {
281+
$age = " [held ${hrs}h; DIRECTORY ONLY -- no live session in it, last commit $($live.QuietHours)h ago]"
282+
$age += " -- ROUTE to the Cleaner/Dispatcher; not releasable on this signal alone"
205283
}
206284
default { $age = " [held ${hrs}h; holder liveness UNKNOWN -- confirm before releasing]" }
207285
}
@@ -246,10 +324,26 @@ if ($Release) {
246324
}
247325
'present' {
248326
Write-Host " HOLDER IS STILL THERE -- that worktree exists and last committed $($live.QuietHours)h ago." -ForegroundColor Red
327+
Write-Host " OCCUPANCY UNKNOWN: the session probe could not run, so this does NOT say whether"
328+
Write-Host " anyone is in it. Treat that as MORE reason to coordinate, not less."
329+
Write-Host " Do NOT -Force it on the strength of a quiet period: a session can be alive and"
330+
Write-Host " simply not committing. Ask that session first -- releasing a live claim is how two"
331+
Write-Host " sessions end up building the same thing."
332+
}
333+
'occupied' {
334+
Write-Host " HOLDER IS STILL THERE -- that worktree exists, a live session is IN it, and it last committed $($live.QuietHours)h ago." -ForegroundColor Red
249335
Write-Host " Do NOT -Force it on the strength of a quiet period: a session can be alive and"
250336
Write-Host " simply not committing. Ask that session first -- releasing a live claim is how two"
251337
Write-Host " sessions end up building the same thing."
252338
}
339+
'unoccupied' {
340+
Write-Host " HOLDER IS A DIRECTORY, NOT A SESSION -- that worktree exists and last committed $($live.QuietHours)h ago," -ForegroundColor Yellow
341+
Write-Host " but NO live session is placed in it. This is the third state (BACKLOG #1348)."
342+
Write-Host " STILL NOT YOURS TO -Force. Nothing on this host can prove a session is gone: occupancy"
343+
Write-Host " sees a session by the cwd it launched in, so one working here BY ABSOLUTE PATH from"
344+
Write-Host " elsewhere is invisible to it. This is reported so you can ROUTE it, not act on it."
345+
Write-Host " Route to the Cleaner or the Dispatcher -- releasing another worktree's claim is theirs."
346+
}
253347
default {
254348
Write-Host " HOLDER LIVENESS UNKNOWN -- the worktree exists but could not be dated." -ForegroundColor Yellow
255349
Write-Host " Confirm with that session before using -Force."
@@ -425,10 +519,26 @@ try {
425519
}
426520
'present' {
427521
Write-Host " HOLDER IS STILL THERE -- that worktree exists and last committed $($live.QuietHours)h ago." -ForegroundColor Red
522+
Write-Host " OCCUPANCY UNKNOWN: the session probe could not run, so this does NOT say whether"
523+
Write-Host " anyone is in it. Treat that as MORE reason to coordinate, not less."
524+
Write-Host " Do NOT build it in parallel -- that is the duplicate-work this gate exists to stop,"
525+
Write-Host " and do NOT -Force it: quiet is not dead. Coordinate with that session or pick"
526+
Write-Host " different work. Its note above says what it is doing."
527+
}
528+
'occupied' {
529+
Write-Host " HOLDER IS STILL THERE -- that worktree exists, a live session is IN it, and it last committed $($live.QuietHours)h ago." -ForegroundColor Red
428530
Write-Host " Do NOT build it in parallel -- that is the duplicate-work this gate exists to stop,"
429531
Write-Host " and do NOT -Force it: quiet is not dead. Coordinate with that session or pick"
430532
Write-Host " different work. Its note above says what it is doing."
431533
}
534+
'unoccupied' {
535+
Write-Host " HOLDER IS A DIRECTORY, NOT A SESSION -- that worktree exists and last committed $($live.QuietHours)h ago," -ForegroundColor Yellow
536+
Write-Host " but NO live session is placed in it. This is the third state (BACKLOG #1348)."
537+
Write-Host " STILL NOT YOURS TO -Force, and the refusal is deliberate: occupancy can VETO but never"
538+
Write-Host " authorise, because nothing here can prove a session is gone. A session working in this"
539+
Write-Host " path BY ABSOLUTE PATH from another cwd does not appear as an occupant."
540+
Write-Host " Hand it to the Cleaner or the Dispatcher with this line; do not build it in parallel."
541+
}
432542
default {
433543
Write-Host " HOLDER LIVENESS UNKNOWN -- the worktree exists but could not be dated." -ForegroundColor Yellow
434544
Write-Host " Treat it as live: coordinate with that session before -Force."

0 commit comments

Comments
 (0)