feat(worktree): report reapable venvs behind nine conjuncts, delete none - #1244
Merged
Merged
Conversation
added 3 commits
September 17, 2026 10:51
Adds -ReapVenvs to prune-merged.ps1. It identifies reapable `.venv`
directories and PRINTS them. There is no venv deletion path in the
script at all -- not behind -Apply, not behind a confirmation. An
adversarial review returned NEEDS_A_GUARD on every reaper proposed, so
the dry run ships first.
A venv is rebuildable state and a worktree is not, so the two have
different blast radii. The cheaper loss is still fenced by the ENTIRE
worktree conjunction, reused rather than re-derived, plus two more:
C1 venv-present C4 unlocked C7 idle
C2 rebuildable C5 unoccupied C8 merged
C3 fence-avail C6 clean C9 unowned
C2 is `constraints.lock`, which is what new.ps1 installs from, and
deliberately NOT `uv.lock` -- a neighbouring lockfile that no install
here reads. A mutant reading uv.lock is caught by its own test.
C9 is new and is the conjunct the other eight miss. The owning session
id is the six-hex token a Claude-managed slug carries; it looks for
<config-root>/projects/*<id>*/*.jsonl across every .claude* root. NO ID
PARSED AND NO TRANSCRIPT FOUND BOTH MEAN SKIP -- absence of a transcript
is the absence of evidence, not proof of death.
Every count carries its denominator, because a claim of "462 of 462
process module lists read, 0 unreadable" did not survive re-measurement
(464 processes, 222 readable, 242 unreadable). "Nobody was reapable" and
"the check could not run" print different text and different exit codes:
the second refuses the whole pass and exits 2.
Raises the -IdleHours default from 36 to 72. The largest idle reading
ever taken on a worktree somebody was demonstrably in was 34.4h against
a 36h window, so the old margin was 1.6h. 72 is that reading doubled and
outlives a weekend.
Measured read-only against both repositories at this tree:
MessageFoundry: 68 venvs across 109 of 110 registered worktrees,
0 reapable, 51.99 GB (55828257948 bytes) held.
Stopped by C6 clean 45, C7 idle 23.
C9 fails on 68 of 68: 51 trees (37.24 GB) parse no id, 17 trees
(14.75 GB) have an owner who wrote inside the window.
One tree, manager-3aee14-sec-f05-leakgate (0.87 GB), passes both
C5 and C7 while C9 reports its owner wrote 48.85 h ago -- C9
catching what neither liveness signal saw.
korus: 13 of 14 worktrees examined, 0 carry a venv, nothing to fence.
Tests: 19 new, one planted candidate per conjunct that satisfies every
other one, each carrying a REAPABLE positive control in the same
invocation. All 19 fail against the unpatched script. Two mutants were
run as controls: C2 reading uv.lock fails 5 tests, C9 that can never
FAIL fails exactly the 3 C9 tests.
…pted -ReapVenvs computes its verdict BEFORE the apply loop, and it set EXIT_REFUSED from there. $removed does not exist yet at that point, so the guard the -Name path has always carried could not be applied: `-ReapVenvs -Apply` could remove worktrees and still exit 2, which this script's own header defines as "nothing was attempted". An automated caller reads that as a safe no-op. The Set-Exit moves to sit beside the -Name guard, after the apply loop, and takes the same `$removed -gt 0` shape. Reachability was established over the AST rather than by grep: between the old site and the apply loop there is no top-level exit, return, throw or break, and no read of $exit. Nothing destructive changes, and there is still no venv deletion path anywhere. The operator line naming the refusal also moves out of the "if the code is 2" branch, because that is exactly the branch the new guard takes the run out of. Two tests, both red against the unfixed script for the right reason: the first reported "removed 2 worktree(s) and reported exit 2". The refusal cause is C9's absent transcript store rather than -IdleHours 0, so the removing half and the refusing half of the run move independently.
Both copies of the exit-code list still read "2 REFUSED -- nothing was attempted ... (bad cwd, unavailable fence, a -Name that matched nothing)". -ReapVenvs added four refusal causes and neither copy moved: the fence down, transcript roots unreadable, no config root carrying a projects/ directory, and -IdleHours 0 emptying both idle windows. A closed list is what went stale, so it is replaced rather than extended. 2 now reads as per-REQUEST: something you asked for was not attempted. Two cases are named where a run removed worktrees and still refused something -- the -Name and -ReapVenvs paths report 1 once a removal has happened, and a fence that dies part way through the apply loop still sets 2 over removals that already landed. THREE SITES, NOT TWO. prune-merged.ps1 states the same universal twice: the header and the inline comment on $EXIT_REFUSED itself, which a search for the header phrasing walks straight past. docs/WORKTREES.md holds one copy of the enumeration, plus a -ReapVenvs sentence that asserted the pass exits 2. Also, two corrections the same reading turned up: * the -ReapVenvs parameter comment said "It prints a receipt and returns". The block falls through, which is why -ReapVenvs -Apply prunes worktrees and why the refusal code has to be decided after the apply loop. * docs/WORKTREES.md called C1-C8 "the worktree rule above, reused rather than re-derived". One conjunct is dropped: the worktree pass vetoes a tree that contains another registered worktree and the venv pass does not. Defensible -- deleting only .venv cannot orphan a child -- but nothing recorded it.
|
Security failed while this pull request was in the merge queue, so the queue ejected it. Its own head can still be green: the queue revalidates the merge, and the path gates that skip on a pull request run there. Read the run before retrying. https://github.com/MEFORORG/MessageFoundry/actions/runs/35276993902 |
wshallwshall
pushed a commit
that referenced
this pull request
Sep 17, 2026
Hoisting those lines out of "if the run exited 2" fixed when they print and
broke what they claim. `Exit ${exit}:` gets the arithmetic right and the
meaning wrong: on the very run the fence line now exists for -- fence down
AND a directory broken on disk -- it printed
Exit 3: the occupancy fence was unavailable, so nothing was eligible.
Exit 3: a directory is broken on disk RIGHT NOW.
The first is false as a causal statement. The fence sets 2; the broken
directory is what makes it 3. An operator fixes the fence and the code stays
3. The previous commit's own test pinned that wording, so it was about to
become the published reading.
Every tail line now opens with the outcome word, which is already this file's
vocabulary -- the three preamble refusals print bare `REFUSED:` and a failed
removal prints bare `FAILED:`. The run's code stays in the `Done.` summary
and in $LASTEXITCODE, which are the only surfaces that ever knew it.
Not `FAILED (1)`: that would have read as a sibling of the per-candidate
`FAILED (exit $removeExit)` twelve hundred lines up, which carries a git exit
code and is a different thing.
The -ReapVenvs line takes the same treatment rather than staying the odd one
out. It landed with `Exit ${exit}:` in #1244 and has the identical hole.
A GREEN RUN MUST STAY SILENT, AND NOTHING PINNED IT. Before the hoist that
was structural. Now it holds only because the sites setting those conditions
also raise the code, and every assertion in the file was a presence check.
test_a_clean_run_prints_no_exit_code_line_at_all checks absence, with the
positive control in the same invocation -- the run prunes two worktrees and
exits 0. Mutated by forcing the -Name line to print unconditionally: the test
fails with "a green run explained a code it did not exit with: FAILED:" while
the control assertion still holds, so the failure is attributable.
THE FENCE TEST READ TWO PROCESSES AND ONE CODE. It established exit 3 from
the -Json run, then string-matched a separate text run whose own code it
never read -- and the script branches on $Json in its preamble refusals, so
equal codes were an assumption. It now reads the text run's returncode.
THE SHA CITATION WOULD NOT RESOLVE. The comment cited edf01a9, which is
reachable from neither origin/main nor HEAD -- guard-reapvenvs squash-merged,
so that content is on main as 6896b39. A fresh clone gets `fatal: bad
object`. Same family as this repo's rule against citing a #N you have not
allocated. Replaced with #1244 (6896b39), which is an ancestor of both. The
control for that reading is tests/test_worktree_prune_merged.py's own
a824cc7 citation, which IS an ancestor of main.
Three sites still defined 2 as "nothing was ATTEMPTED", which the base
retracted and this branch did not restore -- a -Name miss on a dry run
attempts the whole decision pass and still exits 2. What 2 promises is that
nothing was REMOVED. Corrected at prune-merged.ps1's venv-placement comment
and at two sites in the venv test. None was introduced here; one sits twelve
lines from code this branch touches.
104 tests pass in the file.
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.
Virtual environments are 19.66 GB, 83 percent of a 63 GB worktree class.
.venv/is gitignored andconstraints.lockis tracked, so a venv is rebuildable state: deleting one costs a rebuild, deletinga worktree costs work. Different blast radii, so different gates.
This reports. It deletes nothing. Every adversarial pass over a reaper for this workspace returned
NEEDS_A_GUARD, so the deliverable is the dry run.
There is no venv deletion path, verified three ways
An independent verifier established this before the branch was opened.
Armed first. The removal-verb search fires: run against
scripts/worktree/remove.ps1it returns 3hits including a live
Remove-Item -LiteralPath $f.FullName -Force -EA Stop. The same pattern overthis branch's added script lines returns 0.
Cmdlet inventory over the whole new block:
Test-Path,Get-ChildItem,Get-Item,Join-Path,Split-Path,Measure-Object, the*-Objectfilters, and the script's own helpers. No removal verbof any kind. Every git call it reaches is
rev-parse,for-each-ref,status --porcelainorgh pr list.Asserted on disk, not on the report.
test_there_is_no_venv_deletion_path_anywhere_in_the_scriptruns
-ReapVenvs -Apply, getsREAPABLE, then assertspyvenv.cfgandsite-packages/mod.pystillexist.
The exit-code defect this branch introduced, and fixed
Set-Exit $EXIT_REFUSEDran at line 1223, before$removedis initialised at 1477, so it could notcarry the guard the
-Namepath has at 1717. A-ReapVenvs -Applyrun could remove worktrees andstill exit 2, which the header defines as REFUSED, nothing was attempted.
Reachability was established by walking the AST, not by grep -- every
exit,return,throw,breakandcontinuebetween the two points was classified by whether it sits inside aFunctionDefinitionAst. None can terminate the script or skip the apply loop.Set-Exitis definedonce and has no side effect.
Red before, green after:
The three neighbouring refusal tests run as controls in that second set: they are dry runs and must
stay at exit 2. The refusal cause is C9's absent transcript store, not
-IdleHours 0, so the removinghalf and the refusing half move independently.
Exit 2 is still not a promise that nothing happened, and the docs now say so
A mid-run fence death refuses inside the apply loop and carries no
$removedguard either. A run canremove two worktrees, lose the fence on the third, and exit 2.
That is not fixed here. It is a second behaviour change to a destructive tool's exit codes and it
was not briefed. Both copies of the enumeration now read "something you asked for was not attempted"
rather than a repaired universal, which is true of every path. The
-Namepath carries the identicalhole and is also untouched.
Four sites carried the claim, at three different counts
The fourth was found by searching every mention of "exit" rather than the phrase. The header wraps the
phrase across two lines, so a line-oriented grep misses it -- the surviving occurrences were verified
with a multiline pattern against an
origin/maincontrol that returned 1 and 2.Readings
Baseline was 98; the two new tests are the delta.
git rev-parse HEADandgit status --porcelainwere identical before and after both full runs, so neither measured a moving tree. PowerShell parser:
0 errors. Non-ASCII in the script: 0, against a control on
docs/WORKTREES.mdreturning 185.Live receipt, read-only, one
-RepoRootper repository: 67 venvs across 108 of 109 registeredworktrees, 0 reapable, 51.13 GB held, stopped by C6 clean 42 and C7 idle 25.
What is measured and what is not
C9 has decided nothing yet. It fails on all 67 trees and was the deciding conjunct on zero of
them -- every one was already stopped by a lower-numbered check. Its marginal contribution is
unmeasured, not disproven, and it should not be published as having saved anything.
Nine conjuncts are seven per-tree predicates plus one population gate plus one run-level gate. C3 is
recorded PASS on every row unconditionally, because reaching the row already means it passed.
docs/WORKTREES.md:213now records what the venv pass does NOT reuse from the worktree rule:Get-NestedWorktrees, which vetoes a tree containing another registered worktree. Dropping it issound -- removing a parent orphans its children, which cannot happen when you delete only
.venv--and nothing recorded the drop until now. Confirmed by call-site enumeration at 686, 819 and 1542.