Repository navigation
lane-worktrees sweep: retire a lane's abandoned worktrees, rescue first, never a live writer (#162, part 1 of 2) - #168
Conversation
A swap that does not finish gracefully leaves its writers' trees where they stood, and nothing removed them: a person archived, verified and removed them by hand. `lane-worktrees sweep <lane>` makes that one act, dry run by default, under one rule: nothing is deleted that is not first on origin or in a bundle under the sweeps directory, and a live writer's tree is never touched. Which trees are the lane's is #97's inventory and the disk, never the derived index (Amendment 14(b)); a tree the inventory does not name is FOREIGN and is left unless --include-foreign and a --word, and one another lane's inventory names is never taken. Who may act is #97's reconciliation, read before any write including the fetch: a lane bound elsewhere, held by another live session, managed-owned or unreadable is refused (exit 2); a lane this session holds acts only on the writer count the coordinator states with --live. "Merged" is the register's LANDED line for the branch's PR or gh's MERGED for it, never ancestry alone. Unpublished commits are pushed under the branch's own name, never its upstream's (a branch made from origin/main tracks main), or to rescue/<lane>/<slice>-<UTC> where origin diverged; dirty work becomes a WIP commit built through a copy of the index, so a refused push leaves the tree exactly as it was. Removal re-proves the head and the content at the moment it happens. Also --branches, --include-scratch, --include-caches, --include-sandboxes, --links, alternates and local-remote detection (LOAD-BEARING clones are never removed), sweep --expire with 90-day retention that never expires an archive whose rescue left origin, and the porcelain contract lane-end's gate (#163) reads: 0 nothing to retire, 3 something to retire, 2 refused. Part of #162. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/test_lane_worktrees.py builds a lane's estate under tmp_path: a bare origin reached through a url.insteadOf for a GitHub-shaped URL, the lane's checkout and both worktree roots, and fakes for lanes-edit.sh, gh and tmux. One lane holds a tree in every state, and the dry run is held to the table row for row and to a whole-estate snapshot that does not move. The same lane under --yes is held to every act: pushes, rescue branches seen on origin, the WIP commit's subject and parent, the bundles, the ignored-file archive without bytecode, a manifest that verifies, and one register line per tree. Also: a refused push leaves the dirty tree byte for byte; a live writer (a process's cwd, a tmux pane) is never touched; this session's --yes needs its writer count; bound elsewhere, held elsewhere and unreadable are refused with 2 and change nothing; merged by register and by gh against a branch on main by ancestry alone; no gh, nothing merged; --include-foreign takes a word and never another lane's tree; --branches; scratch, caches and sandboxes; --links; a LOAD-BEARING clone; --expire at 89/90/120 days; the porcelain exit codes; and two cases on the REAL lanes-edit.sh, for the inventory, the register line and a lane bound on another host. Part of #162. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`openRepoTools --install` places `lane-worktrees` at the end of INSTALLABLES, so every index a test takes still names the file it did. The file count moves 16 -> 17 and the artifact count 30 -> 31 wherever they are stated, with the count history extended rather than restated; the receipt sentence now says 31 artifacts, 29 rows. --help names the new command. The hygiene suite compiles `lane-worktrees` beside `lanes-index`: the two shipped commands that are Python and have no .py suffix for the glob to find. Part of #162. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Retiring a lane's worktrees": the rule, which trees are the lane's and who may act, the disposition table as implemented in the order it is decided, the other rows (--branches, scratch, caches, sandboxes, links, bundles), the sweeps directory and its manifest, the one register line per tree, retention and sweep.conf, the porcelain exit contract for lane-end's gate, and the two protocol lines the act assumes, proposed for the amendment that ratifies it. Part of #162. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`os.walk` lstat()s every entry, and the estate on Eagle is tens of thousands of directories: a read-only `--links` dry run took 245 s. A scandir walk reads each entry's kind from the directory read itself, so only a symlink or a `.git` file costs a call of its own. Measured on the same estate: 16.7 s, with the same 403 broken links found (99 of them worktree gitdir pointers). Part of #162. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 4 days by commenting |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Reviewer's GuideIntroduces a safety-first Sequence diagram for a safe lane worktree rescue and removalsequenceDiagram
participant Sweep as lane-worktrees
participant Reconcile as lanes-edit.sh
participant Git as Git repository
participant Origin as Origin remote
participant Archive as Sweeps archive
participant Register as Lane register
Sweep->>Reconcile: lane-reconcile
Reconcile-->>Sweep: ownership and writer state
Sweep->>Git: fetch
Sweep->>Git: git add -A on a copy of the index
Sweep->>Git: commit-tree
Sweep->>Origin: push rescue branch
Origin-->>Sweep: rescue branch visible
Sweep->>Archive: write bundle and manifest
Sweep->>Git: re-read head and verify tree
Sweep->>Git: git worktree remove path
Sweep->>Register: lanes-edit.sh log NOTED
State diagram for lane worktree sweep dispositionsstateDiagram-v2
[*] --> Inventory
Inventory --> Foreign: not in lane inventory
Inventory --> GateRefused: ownership or reconciliation refused
Inventory --> Live: live writer detected
Inventory --> Unsafe: locked, dirty dependency, nested repo, or load-bearing
Inventory --> Candidate: eligible tree
Candidate --> Rescue: dirty or unpublished work
Candidate --> Remove: clean and recoverable
Candidate --> Prune: registered path is gone
Rescue --> Remove: rescue pushed and bundled
Remove --> Noted: removal verified
Prune --> Noted: registration removed
Foreign --> [*]
GateRefused --> [*]
Live --> [*]
Unsafe --> [*]
Noted --> [*]
Flow diagram for the lane-worktrees sweep decision processflowchart TD
A[lane-worktrees sweep lane] --> B[Read inventory and disk]
B --> C[Read lane-reconcile before writes]
C --> D{Gate refused?}
D -->|Yes| E[Report refused and exit 2]
D -->|No| F[Classify trees and related items]
F --> G{Live writer or foreign?}
G -->|Live| H[Report live and never touch]
G -->|Foreign| I[Report foreign and leave]
G -->|Candidate| J{Recoverable before removal?}
J -->|Dirty or unpublished| K[Rescue to origin and bundle]
J -->|Clean and safe| L[Remove or prune registration]
J -->|Unsafe or load-bearing| M[Keep with reason]
K --> N[Re-check head and tree]
L --> N
N --> O{--yes?}
O -->|No| P[Dry-run table]
O -->|Yes| Q[Archive evidence and write NOTED]
P --> R[Porcelain summary and exit 0 or 3]
Q --> S[Exit 0 or 1]
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Unresolved cleanup safeguards can delete active resources or recovery data without sufficient ownership, liveness or preservation checks.
Review effort: Balanced
Findings: 16
Open (23)
Require PR merges into the intended base branch · New Prioritize protection of outstanding open PRs · New Treat failed commit-distance reads as unknown · New Protect clones with external linked worktrees · New Abort cleanup when ignored-file inventory fails · New Protect competing inventory claims from tree removal · New Fetch all branch-sweep checkouts before destructive actions · New Protect foreign trees and unknown-liveness caches · New Allocate unique archive directories per sweep · New Avoid pruning unrelated registrations on removal failure · New Require fresh successful liveness checks before rescue and removal · New Prevent ref deletion after branch movement · New Recheck liveness and archive contents before scratch deletion · New Recheck owning tree liveness before cache deletion · New Verify rescue branch SHA before archive expiration · New Retain archives when rescue ledger verification fails · New Canonicalize lane names before deriving paths and ownership · New Refuse cleanup when inventory records are unreadable · New Allow origin-less bundle-only cleanup with --yes · New Count branch deletion failures in sweep results · New
And 3 more that still need to be addressed.
What changed in this PR
Adds the core lane-worktree retirement command for #162, providing cleanup and recovery support ahead of lifecycle integration.
Changes:
- Adds dry-run planning, rescue, cleanup and archive retention.
- Installs
lane-worktreesas the seventeenth shipped file. - Documents behavior and adds isolated regression coverage.
| File | Description |
|---|---|
tests/test_repo_hygiene.py |
Compiles the new Python command. |
tests/test_openrepotools_command.py |
Updates installer and help expectations. |
tests/test_lane_worktrees.py |
Adds sweep and recovery tests. |
tests/test_install_skill_and_hook.py |
Updates artifact-count wording. |
README.md |
Updates installation documentation. |
openRepoTools |
Installs and advertises the command. |
lane-worktrees |
Implements sweep planning and execution. |
docs/README-lanes.md |
Documents safeguards, options and exit codes. |
AGENTS.md |
Updates the shipped-file count. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Every one of these let a destructive act rest on a read that was stale, failed, or answered a different question: - Merged now needs the PR's base to be the default branch (origin's HEAD, else main/master): a merge into a release branch is not a landing. An OPEN PR protects its branch before any older merged PR on that branch counts, unless the register LANDED that very PR. - A failed `rev-list` distance is a read error, never "0 ahead", for a tree and for --branches' upstream state alike. - A clone whose git directory linked worktrees share is kept. Removing it would take their repository with it. - A tree that another lane's inventory also names is kept. An inventory record that cannot be read refuses the sweep (exit 2) instead of being skipped. The lane name goes through `canon-lane` before any path is derived from it, and an unreadable alias table refuses. - Liveness is asked again before the rescue and again just before removal, afresh (processes, tmux, the recording session). A scan that cannot be made leaves the tree. Scratch is removed only if no writer arrived and nothing outside its caches changed while the tar ran. A cache is rechecked against its owning tree. - Caches never come from a FOREIGN tree, a live one, or one whose liveness is unknown. - Branches are deleted at the SHA that was judged: `update-ref -d <old>` locally, a --force-with-lease push for the remote. Each failure is counted, and the register claims "+ remote" only for a remote delete that happened. - --branches fetches the lane's own checkout too, and a failed fetch deletes nothing. - A stale registration is removed by `git worktree remove <path>` alone, never by a repository-wide prune. - An ignored-file listing that failed stops the act. It is never read as "nothing ignored". - Each invocation gets its own archive directory, created exclusively. - An origin-less worktree under --bundle --yes is bundled and removed, as its dry run says, instead of being read as a failed fetch. - --expire requires the rescued SHA itself on origin and an archive that is whole: a MANIFEST.sha256 listing every file at its digest, and a rescues.tsv whose rows parse. Ten new or extended cases. Each one fails against the previous head and passes here. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The manual now states what changed: the canonical lane name, refusal on an unreadable record, contested trees, the single-registration prune, the linked-worktree clone guard, merged-into-default with open PRs first, SHA-fenced branch deletes, liveness asked again before removal, scratch and cache rechecks, the exclusive archive directory, and expiry of whole archives only. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The merge from #168 brought in archive_ledger and the exact-SHA check. The report's archive section reads them through expiry_rows, so its docstring now says it keeps archives that are not whole and those whose rescue branch moved. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Unresolved ownership, liveness, and content-preservation checks can allow deletion of live or unarchived work.
Review effort: Balanced
Findings: 7
Open (8)
Unreadable /proc entries can hide live writers · New Submodule ignored files can be deleted without archiving · New Unreadable competing inventories allow ownership conflicts · New Reappeared prune paths can delete restored worktrees · New Ignored file changes after archiving can be lost · New Stale sandbox owner checks can delete active sandboxes · New Failed tracking reads can schedule caches for deletion · New Missing manifest entries are not rejected during verification · New
Resolved since last review (23)
Retain archives when rescue ledger verification fails Verify rescue branch SHA before archive expiration Recheck owning tree liveness before cache deletion Recheck liveness and archive contents before scratch deletion Prevent ref deletion after branch movement Require fresh successful liveness checks before rescue and removal Avoid pruning unrelated registrations on removal failure Allocate unique archive directories per sweep Protect foreign trees and unknown-liveness caches Fetch all branch-sweep checkouts before destructive actions Protect competing inventory claims from tree removal Abort cleanup when ignored-file inventory fails Protect clones with external linked worktrees Treat failed commit-distance reads as unknown Prioritize protection of outstanding open PRs Require PR merges into the intended base branch Require ownership and dead-owner evidence for tmp cleanup Report branches when ancestry reads fail Only record successful remote deletions Count branch deletion failures in sweep results
And 3 more resolved.
Seven of round 2's eight. Each one let an act proceed on an unknown: - Another lane's inventory record or directory that cannot be read, or that names no path, now refuses the sweep. Who else claims a tree is read before the fetch, so the refusal writes nothing. - A submodule's ignored files (an .env) keep the tree. The tree's ignored archive covers the superproject only. - Ignored files are printed before the archive and listed again at removal. A change, an addition or a failed listing leaves the tree. - A prune candidate whose directory reappeared is left for the next sweep to classify. - A pytest sandbox's .lock is read again at the act, and a live pid leaves it. - `tracked()` returns None when a repository's index cannot be read, and such a cache is left. - A manifest entry whose file is gone makes the archive not whole. The eighth, unreadable /proc entries of same-account processes, is a design trade and is filed in #170. On Eagle, 9 such processes exist at any time (6 `timeout`, 3 `ssh-agent`), so reading them as "unknown" would keep every tree on every sweep. Four new or extended cases. Each fails against ae17742 and passes here. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
Unresolved data-preservation risks in destructive cleanup require human review and disposition.
Review effort: Balanced
Findings: 5
Open (5)
Publication check misses unpublished commits reachable only from tags · New Submodule checks miss unpublished branches and stashes · New Mirror fetch refspec can make prune delete local-only branches · New Scratch cleanup can delete nested protected worktrees · New Expiry can delete origin-less detached commits with empty rescue ledger · New
Resolved since last review (8)
Failed tracking reads can schedule caches for deletion Stale sandbox owner checks can delete active sandboxes Ignored file changes after archiving can be lost Reappeared prune paths can delete restored worktrees Unreadable competing inventories allow ownership conflicts Submodule ignored files can be deleted without archiving Unreadable /proc entries can hide live writers Missing manifest entries are not rejected during verification
The adversarial review of 3c0b188, reproduced with its probes against the suite's own Estate fixture: 1. --yes and --expire --yes are refused, exit 2, changing nothing, unless LANE_WORKTREES_ENABLE_YES=1 is set. #170 lists data-loss paths that are still open. The suite's Estate sets the variable; a real estate should not until #170 lands. The dry run, --porcelain and --report are untouched. 2. B1: a tree that #97's inventory does not name is still the lane's if it stands under .lane-worktrees/<lane>/, or if it sits in <checkout>/.claude/worktrees on a branch whose own commits carry this lane's Lane: trailer. Another lane's claim still wins. Such trees keep the quiet-hours liveness rule. A lane whose state is NONE but which has trees of its own is refused, exit 2. On Eagle, `sweep openRepoTools-3 --dry-run --porcelain` gave exit 0, `summary 0 15 0 15`. It now gives exit 2, `summary 9 13 4 4`: 9 trees are the lane's, and the snapshot is NONE. 3. B2: subprocess output and git metadata files are decoded with surrogateescape, and stdout and stderr write with it too, so a Latin-1 worktree name is reported rather than raising. An unreadable pytest-of-$USER is a kept sandbox row. A catch-all keeps errors that no read caught inside the contract: exit 2 on a dry run, and under --yes DISPOSITION.md is written first and the exit is 1. 4. B3: every porcelain field escapes backslash, TAB, newline and CR. Worktree registrations are read with `git worktree list -z` where git has it, so a newline in a path no longer becomes a phantom registration. Five new cases (switch off, ownership plus NONE, escaping, non-UTF-8 path, unreadable sandbox root), each failing against 3c0b188. Two cases were moved off the lane root to keep testing FOREIGN trees. 40 cases. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The manual now leads the sweep section with the --yes switch and #170. It also says which unrecorded trees are a lane's and that a NONE snapshot with trees is refused. The exit contract now covers the escaped fields and the catch-all. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
Unresolved ownership and discovery issues, alongside deferred destructive-operation safeguards, require human review.
Review effort: Balanced
Findings: 1
Open (3)
Resolved since last review (5)
Expiry can delete origin-less detached commits with empty rescue ledger Scratch cleanup can delete nested protected worktrees Mirror fetch refspec can make prune delete local-only branches Submodule checks miss unpublished branches and stashes Publication check misses unpublished commits reachable only from tags
5f4bd84 made every unrecorded tree under .lane-worktrees/<lane>/ the lane's. That included standalone clones, which #162's first protocol line and the manual keep FOREIGN ("a lane creates worktrees, never clones"). Copilot flagged it on 296e2f0. Clones are now excluded from both adoption paths. The ownership case adds a clone under the lane root and requires it to stay foreign and not retire-counted. On Eagle none of the lane's seven root trees is a clone, so the dry run is unchanged there. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three files conflicted. Each one was resolved by keeping #121's text and adding this branch's on top: - lane-start: both sides added a section just before "6. launch". #121's 5c (the restart intent is written before exec) comes first, then 5e (the daily report). The report therefore starts only after the intent write, which can refuse the launch. - lanes-edit.sh: both sides extended the unknown-subcommand list. The list keeps #121's five new entries (lane-holders, legacy-restart-check, publish-handoff, restart-intent, set-restart-intent) and adds pathspec-check. - tests/test_lane_helpers.sh: both sides added a section before "nothing real touched". #121's supervised-restart section comes first, then the #162 bytecode section. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bring #168 current with main so the lander can squash it on a green head. The merge was clean: #171 and #168 share only docs/README-lanes.md, where git placed #171's worktrees-index section and #168's lane-worktrees section without overlap, and both stay. #171 adds no placed file, so the install lists and their counts keep lanes-index (#164) and lane-worktrees (#168) as they were. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
…h still points at the start commit it resolved before git ran, so a commit a post-checkout hook makes on it is kept reachable (exit 1, the branch named), and lane-end's gate gives an unreadable lane root exit 1 whatever the sweep answered, not only on 0 (Copilot round 2 on #175). Two new shell-suite cases, each red at 41b5a6a: a hook that commits and then fails, and an unlistable lane root beside an unpublished branch of the lane's own (the sweep answers 3). The third round-2 thread, whether a lane-owned branch with a fully published open PR should hold the gate, is a question about #168's porcelain contract that #174 is changing, filed as #178. Lane: openRepoTools-1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bring #169 current with main after #168 landed as 2080f3d, so the lander can squash #169 on a green head. Main also carries #171 (f5444c5) and #172 (3660224). The merge base is 00971b9 because the squash is not an ancestor of this branch, so git saw #168's files as added on both sides. Four files conflicted: - lane-worktrees (add/add) and README.md: main's copy is byte-identical to #168's old head 87da332, and main changed neither file after it, so this branch's copy (#168 plus #169) is the resolution. - docs/README-lanes.md: the one block is #169's two report subsections against nothing on main's side. They stay, and #171's worktrees-table subsection, which merged cleanly, stays too. - lanes-edit.sh: #171 and #169 each added a usage line, a dispatcher arm before `*)`, and a refusal-list entry. Main's text comes first, then #169's: the `worktrees` line and arm, then `pathspec-check`. The list ends `|set-restart-intent|worktrees|pathspec-check`, so it still matches the arms the repo-hygiene parser reads (64). Changed-line comparison: this merge over main equals #169's own diff, and over 45fa45d equals main's changes since 87da332 (#171 and #172). The only difference in each is the refusal-list line that carries both entries. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests-macos on #168's head df0d691 failed one case, 1160 passed: test_a_path_that_is_not_utf8_is_a_row_not_a_traceback. Its fixture's `git worktree add` of a Latin-1 path died with "fatal: could not create directory of '.git/worktrees/caf\xe9': Illegal byte sequence". APFS refuses a non-UTF-8 name, so no such worktree can exist there, and the case has nothing to report. It failed with a CalledProcessError before reaching its assertion. The fixture now makes the raw-bytes directory first. Where the filesystem refuses it (OSError), or git refuses the add, the case skips and says why. Linux accepts the name, so the case runs there as before: git adds a worktree into an existing empty directory. Refs #177 Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the code (#162, part 2) (#169) * Add lane-worktrees: the sweep that retires a lane's abandoned worktrees A swap that does not finish gracefully leaves its writers' trees where they stood, and nothing removed them: a person archived, verified and removed them by hand. `lane-worktrees sweep <lane>` makes that one act, dry run by default, under one rule: nothing is deleted that is not first on origin or in a bundle under the sweeps directory, and a live writer's tree is never touched. Which trees are the lane's is #97's inventory and the disk, never the derived index (Amendment 14(b)); a tree the inventory does not name is FOREIGN and is left unless --include-foreign and a --word, and one another lane's inventory names is never taken. Who may act is #97's reconciliation, read before any write including the fetch: a lane bound elsewhere, held by another live session, managed-owned or unreadable is refused (exit 2); a lane this session holds acts only on the writer count the coordinator states with --live. "Merged" is the register's LANDED line for the branch's PR or gh's MERGED for it, never ancestry alone. Unpublished commits are pushed under the branch's own name, never its upstream's (a branch made from origin/main tracks main), or to rescue/<lane>/<slice>-<UTC> where origin diverged; dirty work becomes a WIP commit built through a copy of the index, so a refused push leaves the tree exactly as it was. Removal re-proves the head and the content at the moment it happens. Also --branches, --include-scratch, --include-caches, --include-sandboxes, --links, alternates and local-remote detection (LOAD-BEARING clones are never removed), sweep --expire with 90-day retention that never expires an archive whose rescue left origin, and the porcelain contract lane-end's gate (#163) reads: 0 nothing to retire, 3 something to retire, 2 refused. Part of #162. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the sweep against every row of #162's disposition table tests/test_lane_worktrees.py builds a lane's estate under tmp_path: a bare origin reached through a url.insteadOf for a GitHub-shaped URL, the lane's checkout and both worktree roots, and fakes for lanes-edit.sh, gh and tmux. One lane holds a tree in every state, and the dry run is held to the table row for row and to a whole-estate snapshot that does not move. The same lane under --yes is held to every act: pushes, rescue branches seen on origin, the WIP commit's subject and parent, the bundles, the ignored-file archive without bytecode, a manifest that verifies, and one register line per tree. Also: a refused push leaves the dirty tree byte for byte; a live writer (a process's cwd, a tmux pane) is never touched; this session's --yes needs its writer count; bound elsewhere, held elsewhere and unreadable are refused with 2 and change nothing; merged by register and by gh against a branch on main by ancestry alone; no gh, nothing merged; --include-foreign takes a word and never another lane's tree; --branches; scratch, caches and sandboxes; --links; a LOAD-BEARING clone; --expire at 89/90/120 days; the porcelain exit codes; and two cases on the REAL lanes-edit.sh, for the inventory, the register line and a lane bound on another host. Part of #162. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Place lane-worktrees as the seventeenth installed file `openRepoTools --install` places `lane-worktrees` at the end of INSTALLABLES, so every index a test takes still names the file it did. The file count moves 16 -> 17 and the artifact count 30 -> 31 wherever they are stated, with the count history extended rather than restated; the receipt sentence now says 31 artifacts, 29 rows. --help names the new command. The hygiene suite compiles `lane-worktrees` beside `lanes-index`: the two shipped commands that are Python and have no .py suffix for the glob to find. Part of #162. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document the sweep in the lanes manual "Retiring a lane's worktrees": the rule, which trees are the lane's and who may act, the disposition table as implemented in the order it is decided, the other rows (--branches, scratch, caches, sandboxes, links, bundles), the sweeps directory and its manifest, the one register line per tree, retention and sweep.conf, the porcelain exit contract for lane-end's gate, and the two protocol lines the act assumes, proposed for the amendment that ratifies it. Part of #162. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Walk the estate for --links with scandir `os.walk` lstat()s every entry, and the estate on Eagle is tens of thousands of directories: a read-only `--links` dry run took 245 s. A scandir walk reads each entry's kind from the directory read itself, so only a symlink or a `.git` file costs a call of its own. Measured on the same estate: 16.7 s, with the same 403 broken links found (99 of them worktree gitdir pointers). Part of #162. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep a test run's bytecode, cache and temp out of the worktree A suite run left tests/__pycache__, .pytest_cache and, when killed, its sandboxes in whichever worktree ran it; the estate cleanup found those were 968 of 1,000 removals (#162, causes 3 and 4). tests/run.sh now sends bytecode to ${XDG_CACHE_HOME:-~/.cache}/openRepoTools/pycache, turns pytest's cache off, and roots --basetemp and TMPDIR under one run root in ${XDG_STATE_HOME:-~/.local/state}/openRepoTools/tmp/<UTC>-<pid>/ that an EXIT trap removes however the run ends. The suite now runs in the background and is waited for: bash runs a trap only once its foreground child returns, so a TERM used to wait out the whole suite. The INT and TERM handlers stop the suite, then exit, so the EXIT trap removes the run root and, on the mkdir path, the lock. The lock is computed before TMPDIR moves, so it stays the workstation's. The repository venv is used when it has pytest, first on PATH so the command line still reads `python3 -m pytest`. tests/test_run_wrapper.py proves it against a stub suite: the relocation, the run root gone after a pass, a failure, a TERM and an INT, under flock and under the mkdir lock, and the venv. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Name the repository's virtual environment on --install One virtual environment per repository, outside the estate, is the convention the sweep's --include-caches row assumes (#162, cause 3): a venv/ inside a worktree is a leftover to remove. --install now names this repository's, ${XDG_CACHE_HOME:-~/.cache}/openRepoTools/venvs/ openRepoTools, and says whether it is there; when it is not, it prints the two commands that make it. It never makes one: that needs pip and the network, and an install from a checkout needs neither. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Seed the workspace .gitignore with bytecode and cache rules Cause 19 of the cleanup analysis: a handoff's attachments were committed with a whole virtualenv's __pycache__ inside them, and brett-wip carries 703 bytecode paths in its history. `wip init` now adds __pycache__/, *.pyc, *.pyo, .pytest_cache/, .mypy_cache/, .ruff_cache/, node_modules/, .venv/ and venv/ to the seeded .gitignore, in wip_substitute - the one function both step-7 paths go through - so the bytes a seed writes and the bytes a re-run compares are the same. A line the template already carries is not repeated, so a template that adopts them upstream seeds them once. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refuse a workspace commit whose pathspec carries bytecode The .gitignore lines keep a new workspace clean; an older one without them still stages whatever a pathspec names. commit_push now asks git what its pathspec would stage (git add --dry-run, which honours .gitignore exactly as the real add does) and refuses, exit 2, nothing staged, when any path has a __pycache__, cache, node_modules, venv or site-packages component or is *.py[co] - and offers the .gitignore lines that workspace lacks, with the one command that adds them. Attachments are committed by hand, so the same question is a subcommand: `lanes-edit.sh pathspec-check <path>...` answers 0 clean, 2 with the offending paths on stdout and the offer on stderr, 64 usage. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Run the estate report once a day from lane-start The report is the net under every actor that never runs lane-end, and it only works if it is read every day. lane-start now runs `lane-worktrees sweep --all --dry-run --report` once per UTC day per workstation, after the row is written and before the launch: the first start of the day takes report-<YYYYMMDD>.stamp under the state directory with an atomic `set -C` create, removes older stamps, and starts the report detached, stdin, stdout and stderr closed, writing reports/<UTC>.md. Every step either works or is skipped in silence, so the report never delays a start and never fails one. --dry-run starts none; LANE_WORKTREES_REPORT=off is the switch, and the suite sets it. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Report the estate's leftovers with sweep --all --dry-run --report Workspace creation outpaces closeout, and nothing counted it. The report reads every lane's #97 inventory and snapshot (beside each checkout's parent, where #97 keeps them), the workspace register and the disk - never the derived index - and lists, as one markdown document: FOREIGN clones with size, last commit and Lane: owner; orphaned trees of ENDED lanes; unmerged branches with a missing, diverged or unpushed upstream; root-main divergence, with Amendment 4's remedy where only handoff/register paths moved; rescue branches and dirty inventory trees awaiting disposition past aging_days; caches and sandboxes by size; ignored directories over ignored_report_mb; evidence-shaped paths; archives past retention (--expire's own table, now shared as expiry_rows); the workspace's .gitignore and bytecode history; and `status --all`'s findings, reported as what they are and never counted as dirt. The head table carries the rescue-branch count and the sweeps directory's size. It changes nothing: no fetch, no index refresh, no expiry. --post writes it to a file or comments it on owner/repo#n through gh. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document the estate report and prevention at creation The lanes manual gains the report (its sections, --post, the daily run from lane-start and its switch) and what keeps leftovers from arriving: the run.sh relocation, one venv per repository outside the estate, one evidence root per repository under the state directory, and the workspace's bytecode rules with lanes-edit.sh's refusal and pathspec-check. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Take Copilot round 1's defects in the sweep Every one of these let a destructive act rest on a read that was stale, failed, or answered a different question: - Merged now needs the PR's base to be the default branch (origin's HEAD, else main/master): a merge into a release branch is not a landing. An OPEN PR protects its branch before any older merged PR on that branch counts, unless the register LANDED that very PR. - A failed `rev-list` distance is a read error, never "0 ahead", for a tree and for --branches' upstream state alike. - A clone whose git directory linked worktrees share is kept. Removing it would take their repository with it. - A tree that another lane's inventory also names is kept. An inventory record that cannot be read refuses the sweep (exit 2) instead of being skipped. The lane name goes through `canon-lane` before any path is derived from it, and an unreadable alias table refuses. - Liveness is asked again before the rescue and again just before removal, afresh (processes, tmux, the recording session). A scan that cannot be made leaves the tree. Scratch is removed only if no writer arrived and nothing outside its caches changed while the tar ran. A cache is rechecked against its owning tree. - Caches never come from a FOREIGN tree, a live one, or one whose liveness is unknown. - Branches are deleted at the SHA that was judged: `update-ref -d <old>` locally, a --force-with-lease push for the remote. Each failure is counted, and the register claims "+ remote" only for a remote delete that happened. - --branches fetches the lane's own checkout too, and a failed fetch deletes nothing. - A stale registration is removed by `git worktree remove <path>` alone, never by a repository-wide prune. - An ignored-file listing that failed stops the act. It is never read as "nothing ignored". - Each invocation gets its own archive directory, created exclusively. - An origin-less worktree under --bundle --yes is bundled and removed, as its dry run says, instead of being read as a failed fetch. - --expire requires the rescued SHA itself on origin and an archive that is whole: a MANIFEST.sha256 listing every file at its digest, and a rescues.tsv whose rows parse. Ten new or extended cases. Each one fails against the previous head and passes here. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document round 1's rules in the lanes manual The manual now states what changed: the canonical lane name, refusal on an unreadable record, contested trees, the single-registration prune, the linked-worktree clone guard, merged-into-default with open PRs first, SHA-fenced branch deletes, liveness asked again before removal, scratch and cache rechecks, the exclusive archive directory, and expiry of whole archives only. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Say what expiry_rows keeps now that #168 checks whole archives The merge from #168 brought in archive_ledger and the exact-SHA check. The report's archive section reads them through expiry_rows, so its docstring now says it keeps archives that are not whole and those whose rescue branch moved. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Stop the suite's whole process group when a run ends Copilot on #169: a TERM to the pytest pid alone leaves whatever it is waiting on (a shell suite, a git, a fixture's sleep) running, while the EXIT trap deletes its temp root and releases the mkdir lock around it. The suite now starts in a process group of its own, with `set -m` on for that one line only. The INT/TERM handlers and the EXIT trap TERM the whole group, reap the leader, wait up to ten seconds for running members and then KILL the group. Running members are counted from `ps`, never `kill -0`, because an orphan's zombie answers `kill -0` until whoever adopted it reaps it. The kill cases now give the stub suite a child of its own and require it to be gone. Against the previous run.sh that case fails with "the suite's own child outlived its wrapper". Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Offer and seed site-packages/ with the other bytecode rules Copilot on #169: the refusal classifies any path under a site-packages directory, since that is a virtual environment under any name, but neither the offered lines nor the seeded ones covered it. Following the offer therefore left the refusal standing. `site-packages/` now joins BYTECODE_IGNORES and WIP_HYGIENE_LINES. The shell case adds a venv named env-x: it is refused, `site-packages/` is among the lines offered, and it is still refused until that line is added. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Report duplicate clones with no named checkout, over one estate Copilot on #169, two report defects: - Two clones of one origin, neither named for the repository, were both treated as canonical, so the duplicate finding vanished. The checkout is now chosen deterministically (the one named for the repository, else the shallowest), and every other clone is reported, with a note when the choice was ambiguous. - `status --all` was pointed at the estate only when --estate was given. It now always gets the root the report resolved, and lane-start passes its own $PROJECTS_ROOT to the daily run. The report's hygiene check wants site-packages/ too. Each new assertion fails against the previous head. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Take Copilot round 2's defects in the sweep Seven of round 2's eight. Each one let an act proceed on an unknown: - Another lane's inventory record or directory that cannot be read, or that names no path, now refuses the sweep. Who else claims a tree is read before the fetch, so the refusal writes nothing. - A submodule's ignored files (an .env) keep the tree. The tree's ignored archive covers the superproject only. - Ignored files are printed before the archive and listed again at removal. A change, an addition or a failed listing leaves the tree. - A prune candidate whose directory reappeared is left for the next sweep to classify. - A pytest sandbox's .lock is read again at the act, and a live pid leaves it. - `tracked()` returns None when a repository's index cannot be read, and such a cache is left. - A manifest entry whose file is gone makes the archive not whole. The eighth, unreadable /proc entries of same-account processes, is a design trade and is filed in #170. On Eagle, 9 such processes exist at any time (6 `timeout`, 3 `ssh-agent`), so reading them as "unknown" would keep every tree on every sweep. Four new or extended cases. Each fails against ae17742 and passes here. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refuse bytecode that is already staged, not only what add would stage Copilot round 2 on #169: `git add --dry-run` says nothing about a path the index already holds at those bytes, yet the path-limited commit still takes it. bytecode_in_pathspec now also reads `git diff --cached --name-only --diff-filter=d` for the same pathspec. A staged deletion of bytecode is a cleanup and is let through. The shell case force-stages a .pyc after the ignore lines are in place: pathspec-check and the commit path both refuse it, and nothing is committed. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Quote the venv paths in the command --install prints Copilot round 2 on #169: the command is meant to be pasted into a shell, and $XDG_CACHE_HOME is the person's to choose, spaces included. Both paths are now `printf %q`-quoted. The new case installs with a cache directory containing a space, and shlex reads each path back as one word. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Report what the report could not read Copilot round 2 on #169: an unreadable lane snapshot or inventory record became an empty one, so a lane or tree dropped out of every section and the report could look clean. Reads now go through _ls and _record. A failure other than "not there", a tree record naming no path, or an inventory tree whose git status fails is a row in a new section, "Records the report could not read". The unused _sidecar reader is gone. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep AGENTS.md and README.md within their line caps test_repo_hygiene caps AGENTS.md at 316 lines and README.md at 486, and both files were exactly at their caps on main. The run.sh paragraph had taken AGENTS.md to 330 and the venv sentence README.md to 491. The full text already lives in docs/README-lanes.md, so AGENTS.md now says the same thing in the three lines the old sentence took, and README.md says it on the line it extends. No cap is raised. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Assert the flock file only where there is flock Copilot on #169: macOS has no flock, so the wrapper takes the mkdir lock and never creates openrepotools-pytest.lock. The relocation case checked for that file unconditionally and would have failed the macOS job, which is the landing gate. Where there is no flock, the case now checks instead that the mkdir lock was released. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Switch --yes off until #170; keep the gate and exit contract honest The adversarial review of 3c0b188, reproduced with its probes against the suite's own Estate fixture: 1. --yes and --expire --yes are refused, exit 2, changing nothing, unless LANE_WORKTREES_ENABLE_YES=1 is set. #170 lists data-loss paths that are still open. The suite's Estate sets the variable; a real estate should not until #170 lands. The dry run, --porcelain and --report are untouched. 2. B1: a tree that #97's inventory does not name is still the lane's if it stands under .lane-worktrees/<lane>/, or if it sits in <checkout>/.claude/worktrees on a branch whose own commits carry this lane's Lane: trailer. Another lane's claim still wins. Such trees keep the quiet-hours liveness rule. A lane whose state is NONE but which has trees of its own is refused, exit 2. On Eagle, `sweep openRepoTools-3 --dry-run --porcelain` gave exit 0, `summary 0 15 0 15`. It now gives exit 2, `summary 9 13 4 4`: 9 trees are the lane's, and the snapshot is NONE. 3. B2: subprocess output and git metadata files are decoded with surrogateescape, and stdout and stderr write with it too, so a Latin-1 worktree name is reported rather than raising. An unreadable pytest-of-$USER is a kept sandbox row. A catch-all keeps errors that no read caught inside the contract: exit 2 on a dry run, and under --yes DISPOSITION.md is written first and the exit is 1. 4. B3: every porcelain field escapes backslash, TAB, newline and CR. Worktree registrations are read with `git worktree list -z` where git has it, so a newline in a path no longer becomes a phantom registration. Five new cases (switch off, ownership plus NONE, escaping, non-UTF-8 path, unreadable sandbox root), each failing against 3c0b188. Two cases were moved off the lane root to keep testing FOREIGN trees. 40 cases. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document the --yes switch, lane-root ownership and porcelain escaping The manual now leads the sweep section with the --yes switch and #170. It also says which unrecorded trees are a lane's and that a NONE snapshot with trees is refused. The exit contract now covers the escaped fields and the catch-all. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Run the suite in the foreground when stdin is a terminal Adversarial review B7: `set -m` puts the suite in a background process group with the terminal as its stdin. Anything that reads the terminal (--pdb, breakpoint(), input()) is then stopped by SIGTTIN, and the wrapper waits on it for ever while holding the workstation flock. With a terminal on stdin the suite now runs in the foreground, in the terminal's own process group, and a Ctrl-C reaches all of it from the terminal. Without a terminal the process-group stop is unchanged. The new case runs the wrapper on a pseudo-terminal whose stub suite prints a prompt and reads a line. It passes here. Against the previous run.sh it hangs and fails, with the transcript showing the prompt and the typed line that was never read. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Never adopt a standalone clone under the lane's root 5f4bd84 made every unrecorded tree under .lane-worktrees/<lane>/ the lane's. That included standalone clones, which #162's first protocol line and the manual keep FOREIGN ("a lane creates worktrees, never clones"). Copilot flagged it on 296e2f0. Clones are now excluded from both adoption paths. The ownership case adds a clone under the lane root and requires it to stay foreign and not retire-counted. On Eagle none of the lane's seven root trees is a clone, so the dry run is unchanged there. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Skip the non-UTF-8 path case where the filesystem refuses the name tests-macos on #168's head df0d691 failed one case, 1160 passed: test_a_path_that_is_not_utf8_is_a_row_not_a_traceback. Its fixture's `git worktree add` of a Latin-1 path died with "fatal: could not create directory of '.git/worktrees/caf\xe9': Illegal byte sequence". APFS refuses a non-UTF-8 name, so no such worktree can exist there, and the case has nothing to report. It failed with a CalledProcessError before reaching its assertion. The fixture now makes the raw-bytes directory first. Where the filesystem refuses it (OSError), or git refuses the add, the case skips and says why. Linux accepts the name, so the case runs there as before: git adds a worktree into an existing empty directory. Refs #177 Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…esidue Main is 500687c, the squash of #168 and #169, so this branch's history and main's carry the same content under different commits. The merge is taken against this branch's old base 45fa45d, so main's text stands wherever #174 did not itself change it; the result differs from main by exactly #174's own diff. Lane: openRepoTools-1 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Main is 500687c, the squash of #168 and #169, so this branch's history and main's carry the same content under different commits. The merge is taken against this branch's old base 45fa45d, so main's text stands wherever #175 did not itself change it; the result differs from main by exactly #175's own diff. Lane: openRepoTools-1 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>





What
lane-worktrees sweep <lane>— the one act that retires a lane's abandoned worktrees, branches, scratch and killed-suite sandboxes after an ungraceful swap. Dry run by default;--yesperforms it. One rule throughout: nothing is deleted that is not first on origin or in a bundle under${XDG_STATE_HOME:-$HOME/.local/state}/openRepoTools/sweeps/<lane>/<UTC>/, and a tree a live writer owns is never touched. Placed byopenRepoTools --installas the seventeenth file (31 artifacts).This is the first of two stacked PRs for #162 (the core). The second —
tests/run.sh's cache/temp relocation, the per-repository venv, the estate-wide--all --reportrun bylane-start, the<user>-wip.gitignoreand the bytecode refusal — is stacked on this branch.Why
A swap that dies on a usage limit, a killed pane or a crashed harness leaves its writers' trees as they stood, and the protocol's worktree-safety rule (a swap never commits, pushes, stashes, resets or cleans) is right to. Until now nothing retired them afterwards except a person doing it by hand. This makes that procedure one act with a dry run, a rescue before every removal, and one register line per tree.
How it decides
lanes-edit.sh lane-trees) and the disk — never the derived index (Amendment 14 clause (b)). Registrations of the lane's checkout and checkouts under its two roots that the inventory does not name are FOREIGN: reported and left, unless--include-foreign --word "<verbatim>"; a tree another lane's inventory names is never taken.lane-reconcile, read before any write (the fetch included). Bound on another host/container (A18(b): unknown, never dead), held by another live session, managed-owned, or any read failed → refused, exit 2. Held by THIS session →--yesneeds the coordinator's writer count:--live <worktree>per live writer, or--live none(A17 Addendum 1 (i)).LANDEDline for the branch's PR, orgh pr list'sMERGED, into the default branch, and that PR's head must hold the tip. It is nevergit branch --mergedalone (squash merges). An OPEN PR protects its branch before any older merged PR counts.The disposition table, as implemented (first matching row decides)
--yesforeign/proc, elselsof), a tmux pane in it, named by--live, the sweep started inside it, or its recording session liveliveprunegit worktree remove <path>(that one registration)gonekeepload-bearingwip-rescue+removegit add -Ainto a copy of the index →commit-tree→rescue/<lane>/<slice>-<UTC>pushed and seen on origin, bundled, removedremoverescue+removeremove+delete-branchLane:trailer is this lane's and it holds nothing beyond the PRremovemain/master, or origin divergedrescue+removepush+removeAt removal the head is re-read and the tree must still be clean, or byte for byte the rescued tree. A refused push leaves the tree exactly as it was (the WIP commit is made beside it). Ignored non-cache, non-build files (an
.env) are archived first. Also--branches,--include-scratch(tar + sha256 → remove),--include-caches,--include-sandboxes(owning pid gone),--links,--bundle, andsweep --expire(90 days,sweep.conf; never an archive whose rescue branch left origin). Every archive carriesMANIFEST.sha256,DISPOSITION.mdandrescues.tsv; oneNOTEDline per tree acted on, written after the act.The exit contract for #163
lane-worktrees sweep <lane> --dry-run --porcelain:lanerow, optionalrefusedrow, onetree/branch/scratch/cache/sandbox/linkrow each,summary <to retire> <trees> <live> <foreign>; exits 0 nothing to retire, 3 something to retire, 2 refused.--yes: 0 done, 1 an act failed part-way; usage 64.Two defects the real-data dry run caught before review
git worktree add -b X origin/maintracksorigin/main; "push to its upstream" would have pushed a lane's stray commits ontomain. The push target is now always the branch's own name (read_tree, with a comment).--yesthe fetch ran before the lane gate refused, so a refused run still wroteFETCH_HEAD. The gate now decides before any write; the parametrized refusal case snapshots the whole estate.Tests
tests/test_lane_worktrees.py(40 cases after review rounds 1-2 and the adversarial review; fakelanes-edit.sh/gh/tmux, bare origin behind aurl.insteadOf, everything undertmp_path; the two last cases run the reallanes-edit.sh):--yesperforms the table.envarchived without bytecode, manifest verifies, one NOTED per tree with no,/—/reserved wordsrescue/*→ exit 1, tree byte-identical, branch/index unmoved, no NOTED--yesrefused without--live;--live <path>keeps it--yes, estate unchangedmainwith no PR keeps its branchLANES_NO_GITHUB=1--include-foreigntakes a word--word; another lane's tree never taken--branchesrescue/*untouched.lockpid and oldtmp.*removed; young, held and live-lock kept--links--shareddependent → never removed, dependents +repacknamed--expireretention_dayshonouredlane/summaryrowsset-lane-treeinventory → removed; FOREIGN left; the NOTED line lands in the lane's own log--yesrelease/1merge is no landing; open #43 protects despite merged #42; both branches kept--yes, estate unchangedREPOA-1→repoA-1viacanon-lane; 66 refuses--yes→keep, branch intact--bundle --yeskeepPlus the installer/hygiene suites updated for 17 files / 31 artifacts and
lane-worktreescompiled besidelanes-index. Locally every case above was exercised; thetests/run.shrun is queued behind other estates' pytest runs on Eagle, so CI is the record.Measurements — Eagle, lane
openRepoTools-3, read-only dry runs (nothing was run with--yes)sweep openRepoTools-3at7e58b98NONE— no handoff since #97 landed). This exit 0 was the B1 defect: the gate passed a lane with 7 trees under its own rootsweep openRepoTools-3 --dry-run --porcelainat296e2f0refused: snapshotNONE, yet 9 trees are the lane's (7 under.lane-worktrees/openRepoTools-3/, 2 in.claude/worktreesby theirLane:trailer);summary 9 13 4 4sweep openRepoTools-3 --yes --live noneat296e2f0--yes is disabled until opensoft/openRepoTools#170 lands… --include-foreign --branches --include-scratch --include-cacheslive(3 by a process's cwd — incl. the siblinga14-worktreeswriter and this writer's own tree — and 4 FOREIGN active within 24 h); 5remove+delete-branch(nw-a18,nw-a18-add1,nw-a18-add2,nw-a19merged by register LANDED;feat/claude-currentby gh, PR #134); 1remove(001-separate-swap-ctx-handoff, clean, on origin); 2 review branches listed, never deleted; 7 scratch dirs to archive (~148 MB:featlist-scratch86.7 MB,dash-scratch38.6 MB,wb71-scratch19.2 MB,a11-scratch3.2 MB, three small); 0 caches outside the scratch… --links/workspace/projects, 99 of them worktree gitdir pointers… --include-sandboxesmktempsandboxes in/tmp, each over an hour old with no live process standing in, holding or naming itsweep --expireReview round 1 (Copilot, 23 threads)
22 were taken in
bf066fdand documented inae17742. Each new or extended case fails against7e58b98and passes atae17742:rev-listis a read error, never "0 ahead";canon-laneruns first;update-ref -d <old>,--force-with-lease) and every failure is counted;+ remoteis written only when the remote delete happened;--branchesfetches its checkouts, and a failed fetch deletes nothing;--bundle --yesis bundled and removed;--expirerequires the exact rescued SHA and a whole archive (manifest-verified,rescues.tsvparseable).The one not taken (
tmp.*sandboxes, which carry no ownership evidence) is a design question about #162's own table, filed as #170. Every thread was answered and resolved.The read-only dry run on this lane at
ae17742gives the same answer as before (exit 3, 10.5 s): 7 live, 5remove+delete-branch, 1remove, 7 scratch. The open PR's own branchfeat/lane-worktrees-sweepis nowkeep.Review round 2 (Copilot, 8 threads)
7 were taken in
3c0b188. Each new or extended case fails againstae17742:.envkeeps the tree;.lockis re-read at the act;tracked()reports an unknown when it cannot read, and the cache is left;The one not taken (unreadable
/procentries of same-account processes) is #170 item 2. On Eagle there are always about 9 of them (6timeout, 3ssh-agent), so reading them as unknown would keep every tree on every sweep.Copilot's third round (5 threads, after the cap): filed as #170 items 4-8 and resolved. These are real
--yes/--expire --yesdata-loss paths:--prune;They should be fixed before the sweep's first
--yeson real trees. The dry run, which is all #163's gate reads, is unaffected.CI at
3c0b188: green.tests959 passed;tests-no-submodule751 passed, 208 skipped;tests-windows153 passed, 806 skipped;guard-launch-modeandparse-macospass;tests-macoswaits for thereadylabel.The Opus adversarial review of
3c0b188(standing in for Codex, which is over quota)Its verdict was "must not land as is", reproduced with probes against the suite's own Estate fixture. Taken in
5f4bd84and documented in296e2f0:--yesand--expire --yesare switched off (exit 2, nothing changed) unlessLANE_WORKTREES_ENABLE_YES=1is set. The test Estate sets it; the manual says it is not for a real estate until lane-worktrees: review findings deferred from #168/#169 (incl. five --yes data-loss paths found after the two-round cap) #170 lands. The dry run,--porcelainand--reportare unaffected..lane-worktrees/<lane>/, or in.claude/worktreeson a branch whose own commits carry the lane'sLane:trailer, is the lane's. A NONE snapshot with such trees is refused, exit 2. Eagle went from exit 0 to exit 2 (table above).pytest-of-$USERis a kept row. A catch-all keeps errors no read caught inside the contract: exit 2 on a dry run; under--yes, DISPOSITION.md first, then exit 1.\\, TAB, LF and CR, and registrations are read withgit worktree list -z. The escaping is documented in--helpand the manual.There are five new cases (40 in all), each failing against
3c0b188. A1-A11, B4-B6, C1, C4-C6 and E1-E7 are in #170's new table; the--yesswitch stands until they land.Part of #162. Refs #97 #161 #163 #164 brettheap/new-workstation#48
Lane: openRepoTools-3
🤖 Generated with Claude Code
Summary by Sourcery
Add a safety-gated, rescue-first sweep command for retiring abandoned lane worktrees without touching live writers or losing recoverable data.
New Features:
lane-worktrees sweepcommand for dry-run inspection and controlled retirement of abandoned lane worktrees, branches, scratch areas, caches, sandboxes, and links.Bug Fixes:
mainby targeting unpublished work under its own branch name.Enhancements:
Documentation:
Tests:
lane-worktreestest suite covering dry runs, cleanup actions, liveness, ownership, branches, caches, sandboxes, links, bundles, expiration, encoding, and refusal cases.Chores:
lane-worktreesalongside the existing command set.