Skip to content

Amendment 23 S6.2: lane-end --sweep-checkouts removes the register checkouts CLOSED lanes left under <W>-lanes/, and the clones beside W, only where nothing holds them, each decided under its lock after a fetch (opensoft/openRepoTools#198, slice 4) - #21

Draft
brettheap wants to merge 57 commits into
feat/198-a23-slice-1b-guard-publishfrom
feat/198-a23-slice-4-lane-end-sweep

Conversation

@brettheap

@brettheap brettheap commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-2 (openRepoShape-2)
Claim: opensoft/openRepoTools#198 (comment)

Re-homed from opensoft/openRepoTools#308

This is slice 4 of act 2 (Amendment 23), re-homed to the code leg, opensoft/openRepoTools-code, by cherry-pick and not by merge, in the stack order (Brett Heap's Q28 answer, on opensoft/openRepoTools#198, comment 6722244541: slices 2a, 2b, 3 and 4 open here as draft PRs by 2026-11-12). The old PR, opensoft/openRepoTools#308, stays as the archived original; nothing more is pushed to its branch. Below the receipt is the original body, brought up to this round.

Old (opensoft/openRepoTools) Code leg (this PR)
Branch feat/198-a23-slice-4-lane-end-sweep feat/198-a23-slice-4-lane-end-sweep
Slice head when re-homed 7d194593726d9ccb3e645cc132f10f7209bce383 e5063e0db7f1dd1c90c4397401525ff03d7e9009
Base when re-homed (slice 1b's head it last merged) 5588307 02f2d08
Slice 1b's head merged since 34799b7 (slice 1b's code-leg branch, with slice 1a' and the code leg's main 577110e), merged as e402a23
Head of this PR 1f16bbcaac7b855988f600765ca26c975013f1f6

Method.

  • The base map is old f18b818 to code 0cefa1f: same subject, author and author date; filter-repo only rewrote the short shas in its message.
  • Each of the slice's 50 commits (47 plain, 3 merges of slice 1b) was rebuilt with git commit-tree. Its old tree is cut to the code leg's 26 owned top-level paths (the cutover plan's leg: code entries), and its parents are mapped. Author, author date and committer date are kept; the committer is Brett Heap. The message is kept byte for byte, plus a (cherry picked from commit <old sha>) line placed as git cherry-pick -x places it.
  • Slice 4 changes only lane-end and tests/test_a23_sweep_checkouts.py, both on the code leg, so no hunk was dropped and no commit is empty. Its spec patch is empty.
  • A check of every old and new pair (tree, parents, author, committer, message, headers), over all six slices (227 commits), found no mismatch.

Range-diff summary (git range-diff 5588307..7d19459 02f2d08..e5063e0 --creation-factor=60): all 47 plain commits pair, and each pair differs by the added cherry-pick line alone. None is unmatched. The 3 merges are not shown by this git (2.43); the pair-by-pair tree check covers them.

This round, on the code leg (rv4b's pre-review of old 811c10f, here 4a356ac): the last old head, 7d19459, carried MUST A and SHOULDs B, C and D. On the code leg, 2d7f80d closes the 353 gap (decision 27), and f6de965, e572b49, 001bd6d and 28fb2ad are NITs G, H, I and J. Then e402a23 merges slice 1b's code-leg head 34799b7, as the coordinator named it, with --no-ff: no conflict, since the one file both change, lane-end, changes in places apart (1b' brings the code leg's main, with opensoft/openRepoTools#292's ps subshell in dh_settle_gone, outside the sweep). The merge's module run found one name slice 1b's lanes-edit.sh now writes in .git/lanes-edit/ that the sweep did not know, and 1f16bbc is that fix (decision 30).

Reading the original body below. Its shas are old shas; the receipt maps each to its new one. The ones it names most: 210f4cf is ab1109d, 53b7bf3 is 7541a21, e6cb1e8 is c4c8e9b, cd060f0 is 7cf3147, ff12b01 is d22503d, 811c10f is 4a356ac, 80bd392 is b6a517c, and the merges cede054 and 4f31084 are 98595bb and b1c3855. Slice 1b's 5588307 is 02f2d08, d9ff4e6 is c4393e8, e6ea8f8 is e594547, 6ecfc52 is 9714694 and 3905009 is 2ed9e62; slice 1a's 15c8e3f is cb05cfc. "Pushed" there means pushed to the old branch. Ten-digit numbers in parentheses are comment ids in the old repository.

Receipt: the 50 rebuilt commits, old sha to new sha (subjects cut to 56 characters)
Old sha New sha Subject
faac2e7b9 a93dfd882 Amendment 23 S6.2: 'lane-end --sweep-checkouts' lists ...
991d8c6c9 8fa703bd2 Amendment 23 S6.2: 'lane-end --sweep-checkouts --yes' ...
2140189e2 b1eac0d6b Amendment 23 S6.2: 'lane-end --sweep-checkouts --yes' ...
210f4cfdb ab1109d23 Merge slice 1b's head d9ff4e6 (rv1b's MUST 4, MUSTs ...
cede05468 98595bb08 Merge slice 1b's head e6ea8f8 (SHOULD C and the shell ...
b896230dd 3f18f87d3 Amendment 23 S6.2: 'lane-end --sweep-checkouts --yes' ...
2e58431d2 b923cbacc Amendment 23 S6.2: a re-ask under a checkout's lock ...
e152a89da ea6a07d35 Amendment 23 S6.2 tests: the two cases in which a lane ...
dfec3c5c0 36f08a1d2 Amendment 23 S6.2 tests: the path on which W's ...
a4cbf1a0d efc6ff7ee Amendment 23 S6.2: a NOTED write that exits 3 (pushed ...
9a7e8c88b d1dc971f4 Amendment 23 S6.2 tests: the cwd scan and the ...
1345897d7 f07aa026c Amendment 23 S6.2 tests: the ten composite asserts are ...
65594885b 5b285a290 Amendment 23 S6.2: a former name's checkout is kept ...
10468ac67 f233cc665 Amendment 23 S6.2: a '.git' file naming a clone's git ...
a376e5b0b 483a72f46 Amendment 23 S6.2 tests: the '--yes' refusals before ...
459c1f6b2 a0d058ae6 Amendment 23 S6.2 tests: a LIVE lane is kept by the ...
cd060f09a 7cf314729 Amendment 23 S6.2 tests: a lock taken over is left to ...
4f3108449 b1c385585 Merge slice 1b's head 5588307 (A37's keep-if-gone, the ...
67e3433a3 ee326d2d4 Amendment 23 S6.2: a former name's checkout is a ...
95f62f1d8 513364dc7 Amendment 23 S6.2: a sweep whose pid line cannot be ...
5b029c471 7f45bf8d4 Amendment 23 S6.2: a '.git' file whose 'gitdir:' line ...
e6cb1e8a7 c4c8e9b4e Amendment 23 S6.2 tests: the lock's refresh before the ...
78caf60be 8ca153ee5 lane-end --sweep-checkouts: a former name of the ...
53b7bf334 7541a2110 Amendment 23 S6.2 tests: D30's case also fails the ...
e78e9fd81 2f5613cd2 lane-end --sweep-checkouts: a hand edit that ...
50a37b7c5 d6521bc76 lane-end --sweep-checkouts: a checkout with a file ...
596aeeeb3 b097a0a8a Amendment 23 S6.2 tests: a checkout whose own path ...
ff12b0154 d22503da8 lane-end --sweep-checkouts: an interrupted '--yes' lets ...
3373d5be2 b2fee5e4b lane-end --sweep-checkouts: a NOTED write's exit 6 is ...
6b3c308cb cb512871a Amendment 23 S6.2 tests: the '.git/lanes-edit/' ...
4c45223e0 4c73d6d5e lane-end --sweep-checkouts: 'find' decides what in ...
75c711859 6ff075ac6 Amendment 23 S6.2 tests: three guards of the act that ...
24e3c6d6b 1b3de12c4 lane-end --sweep-checkouts: only 'last-session''s 8 is ...
e04d26396 63d64a344 lane-end --sweep-checkouts: what leans on a checkout is ...
dba029417 6018b914e lane-end --sweep-checkouts: the register clones beside ...
433f759be 4511512c0 lane-end --sweep-checkouts: the holds' comment line at ...
9b19e124e a06b2cbaa lane-end --sweep-checkouts: a '<W>-lanes/' that cannot ...
0f564e84a 1ac76163e lane-end --sweep-checkouts: under routing, '--yes' with ...
909dce4c4 a90f7ba39 lane-end --sweep-checkouts: the pid line of the lock ...
1bd64905f dd23c572e lane-end --sweep-checkouts: the aside's name keeps the ...
ed8576fc3 91b4c2b3b lane-end --sweep-checkouts: each control character in a ...
307b9ca51 859e86601 lane-end --sweep-checkouts: commits only a checkout's ...
9e0cbd158 e43c08442 lane-end --sweep-checkouts: a repository at an ignored ...
51ee5ff24 8b28e3c6d lane-end --sweep-checkouts: a sweep-wide lock refuses a ...
811c10fc0 4a356ac90 lane-end --sweep-checkouts: the move aside is the ...
80bd3922e b6a517c92 Amendment 23 S6.2 tests: a CRLF '.git' file whose path ...
c73a9afc6 8025e3851 Amendment 23 S6.2: '--sweep-checkouts --yes' refuses ...
2bc9b6676 92186da6f Amendment 23 S6.2: after '--sweep-checkouts --yes' ...
e12d07763 a90260a4b Amendment 23 S6.2: a directory of this user's own that ...
7d1945937 e5063e0db Amendment 23 S6.2: the dependents scan reads '$TMPDIR' ...

Stacked on

Act 2's slice 1b, feat/198-a23-slice-1b-guard-publish, at 5588307 (it carries slice 1a, opensoft/openRepoTools#282, at 15c8e3f). This branch began at 1b's 3905009 and reached 6ecfc52 by a fast-forward before its first commit. After its three stages it merged 1b's d9ff4e6 with --no-ff as 210f4cf, then 1b's e6ea8f8 as cede054, and then 1b's 5588307 as 4f31084. git found no conflict any time, because slice 4 changes no file slice 1b changes. On the code leg, slice 1b's code-leg head 34799b7, which carries slice 1a' and the code leg's main 577110e, is merged with --no-ff as e402a23, again with no conflict: the one file both change, lane-end, changes in places apart.

Please review the commits after 5588307, here 02f2d08: git log --no-merges 02f2d08..HEAD. They are the three stages, then the fix round for the pre-review (rv4) and its re-check (rv4-r2), then the round for the re-review of that fix round (rv4-c3), one commit per finding, then the round for its re-check (rv4-c4), then the round for lane openRepoTools-3's review at 53b7bf3, one commit per finding: git log --no-merges 7541a21..80bd392 here (old 53b7bf3..80bd392). Its re-check of 351 (391, 392, 394), finding 545, Brett Heap's answer to question 1 (D37) and lane 3's NITs are in it too, and its last commit, tests only, pins again the one mutant the round's battery found alive (c3-3-crlf, decision 25). Then this round, for rv4b's pre-review of 811c10f, one commit per finding: MUST A, SHOULDs D, B and C, the 353 gap (decision 27), NITs G, H, I and J, then the merge of slice 1b's code-leg branch and decision 30: git log --no-merges b6a517c..HEAD here (old 80bd392). SHOULD E is a measurement and a question (decision 28, question 4), and NIT F is decision 25. The slice's whole diff is git diff 02f2d08 HEAD here: lane-end and the new tests/test_a23_sweep_checkouts.py, nothing else.

Decisions

  1. The flag (D1). lane-end --sweep-checkouts [--yes] [--dry-run] is its own act, like --retire-dormant, answered before the lane derivation (which refuses a line naming no lane). It refuses (2) a lane, --retire, --retire-dormant, --force, --inventory-only, --state-was, --home and --reason. --dry-run with --yes is a dry run. The usage, OPTIONS and EXIT text name it.
  2. Nothing outside <W>-lanes/ is removed (D2). Amendment 23 S6.2 sweeps every register clone BESIDE W too (the per-profile clones). The brief this slice was written under says twice that nothing outside <W>-lanes/ is ever a candidate. A removal cannot be undone, and the plan says slice 4 is not needed for the install, so this build takes the stricter rule: it LISTS each clone beside W with what holds it, and removes none. Question 1 below. Superseded by decision 23: Brett Heap answered question 1, and the clones beside W are swept too.
  3. Candidates (D3). An entry directly under <W>-lanes/ that is a clone of the register (a directory at its own real path, not a link, a .git directory of its own, the root of its checkout, origin the same repository as W's, git common directory not W's) whose lane lanes --closed --all classes closed; or a former name's checkout beside its new one (S1), once that lane is CLOSED too (decision 15). A former name with no new one beside it is that lane's own checkout, and is judged as the lane's. Hidden entries (creation locks, .partial clones, the sidecar lock) are never looked at, and are counted.
  4. Never a candidate (D4, D17, D23): W, by real path and by git common directory; a LIVE lane's checkout; the runner's own checkout (the NOTED line goes through it); the checkout a caller's LANES_REPO names (Eagle's lane windows export one); a symbolic link, or anything a link leads to; a <W>-lanes/ that is itself a link. A W that stands in a *-lanes directory is refused (2), as lanes_home_check refuses it: from a lane window, "beside W" would be every other lane's checkout.
  5. The holds, widened where git cannot prove S6.2's test (D5):
    • (a) a + under git cherry refs/remotes/origin/<branch> for every local branch and a detached HEAD, refs spelled in full; a merge commit origin lacks, which git cherry skips; any other local ref (a tag, a note) reaching what origin lacks; a stash; staged or modified files; index entries marked assume-unchanged or skip-worktree, which git status does not look at; untracked files ANYWHERE, since no tool can tell a person's work from scratch; an operation in progress, unmerged entries, a held index.lock; the checkout's own writers' lock; and no origin/<branch> to compare with. Ignored files hold nothing, and are named.
    • (b) another repository (anywhere decision 22's scan looks) whose alternates file names an object directory AT OR UNDER the clone, so a store kept inside it counts (D18); a directory in those places whose .git FILE names the clone's git directory, or one inside it (rv4's NIT 1), read as git reads it: a CRLF line (rv4-c3's NIT c3-3) or a path relative to the directory the file is in; and linked worktrees.
    • (c) a live process whose working directory is at or under it: /proc/*/cwd, or lsof on macOS; with neither, "cannot tell" holds it. A working directory that cannot be read (another user's process, or one that ended) is counted and said, and does not hold. Question 2 below.
  6. Leaves first (D6, D21). The candidates are taken in dependency order, every clone before the ones it borrows from, by passes that place every candidate no unplaced candidate leans on. A cycle places nothing more, and what is left goes last, held by what leans on it. The order is a plan: --yes reads what leans on each candidate again just before its removal, so a held dependent keeps the whole chain it borrows from.
  7. The dry run (D7) fetches, locks and writes nothing. It reads each clone's origin/<branch> as last fetched, and says so. 3 means something is removable, 0 nothing.
  8. --yes decides each checkout again, under its own lock, after a fetch (D8, D14; rv1b's MUSTs 1-2). Per candidate, in D6's order: mkdir its lanes/.lanes-edit.lock with lanes-edit.sh's pid line, so its writers wait; fetch it with an explicit refspec, bounded (a failed fetch holds it); ask the lane's state again (lanes --fetch --closed --lane), and for a former name canon-lane again; read every hold, what leans on it and the working directories again; only then mv it aside to <W>-lanes/.<name>.removing-<UTC>-<pid> (its S1 path is absent at once, so the next write clones afresh) and remove that. A removal that does not finish says so by path and exits 1. The pre-scan is a plan, never a decision.
    • Under the lock, the rest is asked again too (rv4's SHOULD 1 and NIT 4). The working directories and what leans on it are read again (R1). A former name's alias and its new name's checkout are asked again (R3). Whether it is still a clone of the register's origin is asked again after the re-ask. The lock's age is refreshed (touch -c) before the fetch and the re-ask, as git_net's heartbeat does. A pid line that cannot be written into a lock just taken lets that lock go at once and holds the checkout (rv4-c3's NIT c3-2), so the sweep never leaves a lock that names no holder, and a line that is not the sweep's is only ever another writer's. Its test fails the write two ways, since each needs its own check: a lock directory that cannot be written in (no pid file is made), and a pid file whose write fails, as on a full disk, which leaves an EMPTY file that the sweep's rm -f must remove before the rmdir can take the lock away (rv4-c4's NIT c4-2). Its pid line is read again just before the mv: a line naming another holder means the lock was taken over, so the checkout is held and that lock is left to its holder. sc_lock_drop and the exit's cleanup leave such a lock, as mutex_release does. The act's removed and PARTLY lines name the ignored files that went with the checkout (NIT 2).
    • "After a fetch" holds on the failure path too (D26; rv4's MUST 1, rv4-r2's SHOULD 1). lanes --fetch exits 0 when W's fetch fails after ls-remote answered (fell-back), and says so only in its notes. So a W fetch that does not complete, either way, refuses --yes at the listing (1, "Nothing was removed"). Under each lock the re-ask's notes are captured and read, and the checkout is held when they say the logs were read as they stand locally, the session records could not be read, or the register could not be read. A capture that cannot be made or read back holds the checkout too, with that as its reason.
  9. The runner's preflight, before anything is removed (D8, D15, D19): LANES_LANE set (2 without), the lane known by a row or a log (2), no LANES_NO_FETCH=1 (2: S3 refuses a writer whose fetch did not run, ratified choice 2, rv1b's MUST 3), and a transcript uuid for the NOTED line's session field, from LANES_SESSION or last-session (2). lanes-edit.sh's own checks when the line is written are still the decision.
  10. The NOTED line (D9, D15, D20). After the removals, and only if something was removed: ONE lanes-edit.sh log NOTED lane:<runner> line in the runner's own log, written through the runner's own checkout under S3 and S4, in a subshell with LANES_NO_FETCH unset. Its text names what was removed, what was moved aside and not all removed, what was held, and how many clones beside W were left, from names held to [A-Za-z0-9._-], and no host path. Its count, and the summary's, count a removal that did not finish apart from the ones that did (NIT 3), and a run whose only act is a partial removal still writes the line. A line the writer refuses exits 1 with the exact command that writes it by hand. Nothing was lost then: each clone removed held nothing unpublished. A write that did not confirm is not one that did not happen (rv4's SHOULD 2): log's 3 (pushed, not confirmed) may be on origin already, and its 6 is mostly a commit kept in the checkout and not on origin. Writing the line again after either would record the sweep twice, or meet S3's refusal of a checkout holding a commit origin lacks. So both are handed the publish act first (LANES_LANE=<runner> lanes-edit.sh commit "..." with nothing staged, Amendment 23 S4), which sends what origin lacks by content and says so where it lacks nothing. Only a 6 whose publish act finds nothing to send (git add or commit failed) is written by hand. The exit is 1 in every case.
  11. The fetches use LANES_GIT and LANES_GIT_TIMEOUT (D16), the seam and the budget lanes-edit.sh uses, through timeout -k 5 where it exists.
  12. Exits (D10): 0 nothing to remove, or --yes done and recorded; 3 the dry run, something removable; 2 refused; 1 W or the writer not found, a read that failed, a removal that did not finish, or a removal not recorded.
  13. bash 3.2 (D11): no associative arrays, no ${x,,}, ${a[@]+"${a[@]}"} for every possibly empty array, no command <utility> but command -v (Under macOS's bash 3.2, a failing command <utility> in a list or an if condition exits a set -e script: lanes --worktrees dies silently (lanes:317), and wip's .gitignore step can stop (openRepoTools:4014) openRepoTools#292).
  14. Open question 5 (lane-worktrees' classification) is not in this slice (D12). Slice 1a already lists <W>-lanes/* as register checkouts. What is left, classing W and the per-profile clones, would widen this slice into a second file with its own tests; lane openRepoTools-3 answered question 3 that it goes in its own PR.
  15. A former name's checkout is a candidate only once its lane is CLOSED, as every other checkout is (D29, superseding D27; rv4's SHOULD 4, rv4-c3's SHOULD c3-1), at the listing and again in the re-ask under the lock. Since 10-19 the launcher exports LANES_REPO=<W>-lanes/<lane>, and whatever was started before the rename still names the old path. D27 kept the old checkout only while the lane was LIVE, and LIVE is a pid test in this container: it misses a process started from a pre-rename window that outlives that window's session (a landing pilot, with the lane then PAUSED), and a pre-rename window in another container that shares <W>-lanes/. Removing the path then refuses that process's next register write (S3: not a git checkout), a HOLD's release among them, and an open HOLD blocks every landing. So a former name's checkout beside its new one is judged as the lane's own checkout is: kept while the lane is anything but CLOSED, with the reason "a former name of lane X, beside that lane's own checkout, kept until that lane is CLOSED", and a candidate once it is CLOSED. The re-ask under the lock asks both that it is still a former name beside the new checkout and that the lane is still CLOSED. The cost is the old checkout's tree, kept until the lane is CLOSED. The other fix rv4 offered, holding any checkout a live process's LANES_REPO names through /proc/<pid>/environ, was not taken: environ is the environment at exec only, cannot be read for another user's processes or another container's, and does not exist on macOS. (Decision 23 reads it after all, as one more hold on every candidate, where Brett's answer to question 1 makes the clones beside W removable: nothing else names the window that writes through one.)
  16. A former name of the RUNNER's own lane is kept for its own reason (D31; rv4-c4's NIT c4-1). Where lane X is renamed Y and Y ends and runs the sweep, X's checkout is kept by the runner's rule, as before, but it said "kept until that lane is CLOSED" and "its checkout is the one its NOTED line goes through", and neither is true of it: Y is CLOSED already, and the NOTED line goes through Y's own checkout. It now says "a former name of lane Y, which runs this sweep: a process it started before the rename may still write through this path", which is what it is kept for (decision 15). Y's own checkout keeps its reason. Nothing is kept or removed differently; only the reason said.
  17. .git/lanes-edit/ holds, but the writer's own scratch (D33; lane openRepoTools-3's MUST 351, SHOULD 391, NIT 392). S3 step 4's carry keeps a hand edit of the register it could not carry in .git/lanes-edit/hand-edit-<UTC>-<pid>.diff, with the register at origin's version, so git status sees nothing and that file may be a person's only copy. Anything there holds, but a PLAIN FILE named exactly apply.err, fetch.err, ff.err, reset.err (git's words from a write; the push replay's drop writes the last) or removed (the lines a carry removed). find decides the kinds, so a folder, a link or a name with a newline that only carries one of those names holds (392). A folder that cannot be listed, or a find that fails, holds as "cannot be listed". The module pins the five exact names against 12 look-alikes, the -L guard, and, as a drift guard, each name lanes-edit.sh writes there, read from its own text (391). The copy a successful carry leaves (hand-edit-*.diff) holds too until a person removes it; Amendment 23 slice 1b: every writer fetches and fast-forwards under its lock or refuses with nothing written, pushes with no pull and reads origin back, a refused push replays the one edit, and commit with nothing staged is the publish act (#198) openRepoTools#307's companion fix (393) decides its name.
  18. A file system mounted at or under a candidate holds it (D35; SHOULD 371). The mount table (/proc/self/mountinfo's field 5, else mount's output) is read at the listing and again under each lock, just before the move; the candidate's path is spelled with mountinfo's escapes to match it. A table that cannot be read, or names no /, holds every candidate as "cannot tell". rm -rf --one-file-system (GNU) or -x (BSD) is a second layer only, probed once. LANE_END_MOUNTINFO is the test seam. A real bind mount through lane-end was not run: this host has no user namespaces.
  19. A signal never cuts a removal in two, and one that ends the act leaves its record (D36; SHOULD 372). Under --yes, INT and TERM wait while a checkout is moved aside, removed and counted: the mv and the rm run as ( trap '' INT TERM; exec ... ), so a Ctrl-C to the process group cannot stop them halfway. A signal then ends the act; after any removal it prints the NOTED line to write by hand; once the line's write has begun, it says the line may be on origin already and hands out the publish act first. An aside a removal left (.<name>.removing-*) is listed by name in every run as leftover, with chmod -R u+rwX -- <p> && rm -rf -- <p>; no sweep removes one. A removal that did not finish is said before the NOTED write, and the run still exits 1.
  20. Exit 6 of the NOTED write is finished by its cause (D38; SHOULD 373). The write's words are kept as well as shown (LE_TMP, captured inside the write's own subshell, so a signal's trap still speaks on the sweep's stderr). "With origin/ unmoved": the publish act alone. "git add failed" or "git commit failed": git -C <checkout> restore --staged --worktree -- lanes/log/<runner>.md, then the line by hand. The commit kept on HEAD after the push lost its races (or git could not name the commit to drop to): S6.3's archive, git -C <checkout> push origin HEAD:refs/heads/archive/<runner>-stranded-<UTC>, then git -C <checkout> reset --keep refs/remotes/origin/<branch>, then the line by hand. Any other words: the two remedies as before. <checkout> is the caller's LANES_REPO, else <W>-lanes/<runner> under routing, else W. The "no longer on HEAD" refusals keep the generic remedies on purpose: HEAD there is not only this write's commit, so an archive and reset --keep could take another writer's commit off the checkout.
  21. last-session's 8 alone is "names none" (D40; lane openRepoTools-3's finding 545). Any other nonzero exit is a register or a log that could not be read, refused with 1 and "Nothing was removed", naming the exit and the writer's last line of stderr.
  22. What leans on a checkout is looked for where clones are made (D41; SHOULD 352, with NITs 378 and 356). find over three roots, by real path: W's parent to depth 6 (a repository's objects at depth 8), $TMPDIR (or /tmp) and $HOME to depth 4 ($TMPDIR to depth 8 since rv4b's SHOULD C, below), hidden directories included, objects pruned, .git files listed; lines deduplicated, since the roots can nest. Each line is <target> TAB <dependent>, the target first, so a TAB in a dependent's own path stays in its own field (378). A repository at or under the candidate, or under a path gone in this run, goes with it. A scan that cannot finish holds every candidate: a root that cannot be entered, a find error other than a directory it cannot read, an alternates or .git file that cannot be read (356). A directory find cannot read is counted once and said, and holds nothing (question 2's rule); since rv4b's SHOULD B, one of this user's own holds every candidate, below. The dry run's header names the scope. Measured since: decision 28.
  23. The register clones beside W are swept too (D37 as built, D42; question 1, answered by Brett Heap). See question 1 below. They are candidates with no lane, after <W>-lanes/'s in the listing, in ONE leaves-first order with them, judged by every hold a lane's checkout has and one more: a live process whose LANES_REPO names the clone (read from /proc/<pid>/environ by the shell itself, NUL by NUL; as spelled, relative ones against the process's working directory, and by real path). A process whose environment cannot be read is counted and said, and holds nothing; with no /proc, a clone beside W is held as "cannot tell" (LANE_END_PROC is the test seam). The clone the caller's LANES_REPO names is kept. Under its lock a clone is asked again whether it is still a register clone (no lane state is asked of it). It is moved aside beside W, never into <W>-lanes/, and an aside left there is listed by every run. The NOTED line names the clones beside W it removed apart from the lane checkouts, each character outside a lane name's written as ?, and counts those left; the dry run's and the act's summaries count both places. This supersedes decision 2.
  24. Lane openRepoTools-3's NITs (D43; 353-355, 358, 359, 375-377, 379, 394), one commit each, each red first:
  • 394: the holds' comment line at 93 columns is wrapped, and test_the_sweeps_comments_are_80_columns_or_less holds every comment line of the sweep's section to 80.
  • 377: a <W>-lanes/ that cannot be entered or listed, or whose real path cannot be read, refuses the run with 1 ("Nothing was removed") before anything beside W is looked at. It read as a link to nothing, and exited 0.
  • 376: under LANES_ROUTE_WRITES=1, --yes with a LANES_REPO whose real toplevel or git common directory is W's is refused with 2 before anything is removed, in S5's words, naming <W>-lanes/<runner>/. Without routing that is the default write path, and is left. The dry run is not refused.
  • 359: the lock's pid line is cleaned as lanes-edit.sh's lock_ident cleans its own: [A-Za-z0-9._:-] kept, an empty container none and an empty host unknown, and either one unknown where it holds anything else.
  • 379: the aside is .<the name's first 48 characters>.removing-<UTC>-<pid>, so a long name is moved aside instead of held in every act.
  • 358: every entry line (keep, hold, remove, removed, PARTLY, leftover, gone) prints each control character of its path and reason as ?, so a name with a newline writes no line of its own. The commands keep %q.
  • 354: commits only the reflog reaches are named beside the checkout, as the ignored files are, and do not hold it: a write replayed after a lost push race leaves one whose patch origin has.
  • 355: an ignored directory that is a repository of its own, an ignored *.swp, and an ignored directory find cannot look in each hold. Annotated tags are named. The comment that called ignored files the register's own rules says what --exclude-standard reads.
  • 375: --yes takes <W's git common directory>/lanes-sweep-checkouts.lock (mkdir, pid line <pid> <ident>) before the listing, and the exit lets it go. A second --yes is refused with 2, naming the lock, its pid line, and the rm -rf for one a killed sweep left. It is in W's git directory, not <W>-lanes/, because the clones beside W are swept where there is no <W>-lanes/ at all. A candidate that is not there at its turn is printed gone: neither removed nor held, and not in the NOTED line.
  • 353: the move aside is the commit point, as mutex_steal's rename is. After the mv and before the rm, the aside's lock pid line, the working directories and what leans on it are read again; an alternates or .git target that is gone is kept as written, and what is inside the aside is skipped. Any of them moves the checkout back and holds it ("...; it was moved back"), or, where the move back fails, "...; it could not be moved back, and is at ". The lock is refreshed just before the pid read before the move. The cost is a second dependents scan per removal.
  1. c3-3-crlf, the one survivor of the battery at 811c10f: the CR handling is not redundant, so the case was strengthened and the mutant kept (D44). NIT 353 kept a .git or alternates target that does not resolve as written. A CR kept after an absolute gitdir: <c>/.git gives <c>/.git<CR>, still under <c>/, so the clone was held either way. A relative path, as git writes a submodule's .git file, resolves only once the CR is gone. So the case took a CRLF line with a relative path, and the mutant was pointed at it. The hold's reason was not changed to name the gitdir: that would change lane-end's output for a test's sake, and a kill on behaviour (remove for hold) is stronger. Decision 27 moves the kill again.
  2. W is the directory workspace.yaml's path: names; LANES_WORKSPACE_ROOT stays the seam, and --yes refuses where the two differ (D45; rv4b's MUST A on 811c10f).
  • The refusal. --sweep-checkouts --yes exits 2 when LANES_WORKSPACE_ROOT is set and its real path is not the real path of path:. This comes before the sweep-wide lock and before any listing. path: is read from the file by lanes_ws_field, which no LANES_WORKSPACE_ROOT reaches. No workspace.yaml, or a path: that cannot be entered, counts as different. The words name the variable, W as it resolved and path:, and say to unset it or point it at path:. The dry run still runs, and its header says that --yes refuses there.
  • Never a candidate (sc_is_a_w, asked at the listing and again under the clone's lock): the directory path: names, wherever it stands; and a clone beside W with a <name>-lanes/ (or a link of that name) beside it, which is a W with lane checkouts of its own, not a per-profile clone. So even a dry run with a wrong override keeps the real W, by its reason.
  • Why the seam stays. The shared resolver is byte-identical in the four lane helpers, and tests/test_repo_hygiene.py holds it there, so lane-end cannot drop the override alone. tests/test_lane_helpers.sh points every helper at one sandbox through it (it never runs --sweep-checkouts), and the module's cases use it. A value whose real path is path:'s, as spelled or through a link, is no refusal.
  • Not done: the refusal compares real paths only, as rv4b's fix asks. It does not re-run the resolver's checks on path: (a git checkout whose origin is repository:).
  1. A target that does not resolve is spelled from the deepest directory on it that does, and the read after the move matches the .git files too (D46; the 353 gap, decision 25's note).
  • After the move, a relative .git or alternates target through .. that named the moved checkout was kept as written, with its .., and named no candidate, so a clone that came during the last reads with such a path did not move the checkout back. sc_real_or_text now gives the real path where the target resolves, and otherwise the real path of the deepest directory on it that stands, with the rest appended as written. So it names the old path again. An absolute target reads as before.
  • The rest is not normalised as text (.. taken away). git itself refuses an alternates path through a directory that is not there ("unable to normalize alternate object path", tried on this host), so no working dependent has one, and keeping the .. can only hold more, never less.
  • The read after the move never matched the .git files. It matched only the alternates lines: sc_gitfile_lines was read under the lock alone. So a .git file written between the last read and the move, absolute or relative, did not move the checkout back, and it was removed under the directory that works in it. That was the one case still red on 10-24. The read after the move now matches the .git files by the alternates' rule (nothing that went with the aside counts), and says "its git directory is named by the .git file of , which works in it and came as it was moved aside". A new case writes an absolute .git file after the last read.
  • With a CR kept (the c3-3-crlf mutant), a relative CRLF line now reads <clone>/.git<CR>, under the clone, so decision 25's case no longer kills it. A CRLF line whose last name is a link to the clone's git directory resolves only once the CR is gone. That case is added to GIT_FILES, and c3-3-crlf is pointed at it. Decision 25's case stays as a behaviour case.
  1. SHOULD E (rv4b's pre-review of 811c10f): the dependents scan was measured, not redesigned; a --yes sweep of the 139 clones beside W is question 4 below (D47).
  • The numbers (the scan's own find, read only, at the depths the sweep uses since SHOULD C, 2026-10-23T20:33-21:30Z, with Eagle at load 26-50 from other lanes throughout, so a high reading, not an idle one): W's parent /workspace/projects to depth 8, 813.7 s (58,825 matches: 45,674 object directories, 988 alternates files, 13,151 .git files); /tmp to depth 10, 1,754.6 s (273,072 matches); $HOME to depth 6, 53.7 s (46 matches). One full scan: about 2,622 s, 44 minutes.
  • What --yes runs: one scan at the listing, one under each candidate's lock, and one after each move aside. For the 139 clones beside W that is up to 1 + 2 x 139 = 279 scans, about 203 hours at these numbers.
  • And the lock: a scan under a candidate's lock outlasts the lock's 10-minute stale time, so another container's writer could take the lock over meanwhile. The pid line read after the move catches that and holds: no wrong removal, but a sweep that holds where it should remove.
  • Not redesigned without lane openRepoTools-3's ruling.
  1. rv4b's NITs G-J, as built, and /proc/<pid>/fd not read, by decision (D48).
  • G: the move back is not changed, since BSD mv has no -T; where the aside went is asked after it. <old path>/<aside name> standing means a directory came to stand at the old path and took the checkout inside it. The row says so and names where the checkout is (move it out by hand), and the lock is not let go. Otherwise "it was moved back", as before.
  • H: five composite asserts are split in two (rv4b's four, and old.is_dir() and new.is_dir()); text only.
  • I: the usage line, exit 3 and the refusal of a lane name the register clones beside W too. The header's "test seam" text was rewritten by MUST A (decision 26).
  • J: the header's processes line names the container and host whose processes were read (lanes-edit.sh workstation, now read in the dry run too), and says that another container's processes, their working directories and their windows' LANES_REPO are not seen.
  • /proc/<pid>/fd is not read, by decision this round. A process that holds a file of a checkout open, with its working directory elsewhere, is not seen. Reading every process's open files is another scan of every pid, it sees only this container's processes, as the working-directory scan does, and its cost was not measured. The lock, the working directories, the windows' LANES_REPO and the dependents are the holds. Question 2 names it as a limit.
  1. A claim's whole Rule 1 reads in .git/lanes-edit/ are the writer's scratch (D49; found by SHOULD 391's drift guard when slice 1b's code-leg branch was merged). Slice 1b's 706 (on Amendment 23 slice 1b: every writer fetches and fast-forwards under its lock or refuses with nothing written, pushes with no pull and reads origin back, a refused push replays the one edit, and commit with nothing staged is the publish act (#198) openRepoTools#307) keeps a claim's Rule 1 reads whole in .git/lanes-edit/claim-reads-<UTC>-<pid>.txt for the claim's re-run, and the claim's last run removes the file; only a killed claim leaves one. The guard, which reads its cases from lanes-edit.sh's text, failed on that name at the merge (hold for remove): a checkout with a leftover one would have been held for good. A plain file whose name starts claim-reads- is now scratch too. It is a copy of what GitHub keeps, so removing it loses nothing; a folder of that name, or a name with anything before claim-reads-, still holds (NIT 392's rule). New case: such a file alone holds nothing; one more look-alike and one more folder case.

What changed

  • lane-end:
    • the --sweep-checkouts flag, its refusals, and the usage, OPTIONS and EXIT text;
    • a new section, AMENDMENT 23 S6.2: --sweep-checkouts, answered before the lane derivation: the W and <W>-lanes/ reads (sc_real, sc_git_common, sc_is_register_clone); the lanes' state from lanes --closed --all and its notes (sc_lookup); the runner and its preflight; the candidates and the keep lines (sc_judge, sc_judge_lane); the clones beside W; the holds (sc_holds, sc_dep_scan with its .git files, sc_dependents, sc_cwd_scan, sc_cwd_inside, sc_judge_holds); the guard on every path (sc_under_home); the lock (sc_lock_take, sc_lock_beat, sc_lock_holder, sc_lock_drop, released by the exit trap unless it was taken over); the fetch (sc_fetch) and the re-ask (sc_still); the order (sc_leaned_on_by_unplaced); the report; the act; and the NOTED line;
    • the "the sweep's own flags belong to the sweep" check knows that --yes also belongs to --sweep-checkouts;
    • lane openRepoTools-3's round adds to the section: the .git/lanes-edit/ hold; the mount table (sc_mount_scan); the signals held off around a removal (sc_sig_defer, sc_sig_arm, sc_on_signal); the NOTED write's kept words and its exit 6 by cause (sc_log_noted, sc_wco); the dependents scan's three roots (sc_dep_root, sc_in_gone); the clones beside W and the LANES_REPO hold (sc_repo_scan, sc_repo_inside); the commits and tags named beside a checkout (sc_named); the sweep-wide lock; and the reads after the move. The usage text says what the sweep removes beside W, and cleanup lets the sweep-wide lock go;
    • rv4b's round adds: W as path: names it, and --yes refused where LANES_WORKSPACE_ROOT names another directory (sc_is_a_w, MUST A); a directory of this user's own the dependents scan cannot read holds every candidate (SHOULD B); $TMPDIR scanned to depth 8, and the scope line names each root's depth (SHOULD C); the windows' LANES_REPO and the mounts read again after the move (SHOULD D); sc_real_or_text, and the .git files matched after the move (decision 27); the move back asked where it went (NIT G); the usage line, exit 3 and a refusal naming the clones beside W (NIT I); and the header's processes line (NIT J); and claim-reads-* as the writer's scratch in .git/lanes-edit/ (decision 30).
  • tests/test_a23_sweep_checkouts.py (new), on tests/a23_box.py's RegisterBox, in Make lane-end's synopsis say what a DUPLICATE retire does, read a state cell that breaks the phrase's grammar whole instead of narrowing it, and correct the TAKEOVER grammar table and two tests.yml header comments openRepoTools#202's ended style. No fixture is copied.

Tests

Module Before, at 6ecfc52 After
tests/test_a23_sweep_checkouts.py (new) not there 198 passed of 198 at 1f16bbc, on the code leg; 193 passed at 28fb2ad, before the merge of slice 1b; at the merge e402a23, 194 passed and 1 failed, the drift guard decision 30 answers (83 at 53b7bf3; 80 at e6cb1e8; 73 at cd060f0; 47 at 210f4cf)
tests/test_repo_hygiene.py 247 passed at 1f16bbc, on the code leg: 235 passed, 18 skipped (cases for files of the spec leg or of the upstream/openRepoShape submodule, which a standalone code-leg checkout lacks); 253 passed at 53b7bf3 (slice 1b's d9ff4e6 adds 6 cases)
tests/test_guard_launch_mode.py 13 passed 13 passed at 53b7bf3
tests/test_test_roots.py 39 passed, 1 skipped 39 passed, 1 skipped at 53b7bf3
tests/test_precision_90a_96.py 25 passed, 1 skipped 25 passed, 1 skipped at 53b7bf3
CI's tests job (the whole suite with the shell suite, Ubuntu 26.04 image) – at ff12b01 (run 39805790284): 3215 passed, 1 failed; at e6cb1e8 (run 39750439670): 3159 passed, 75 skipped, 1 failed
CI's tests-no-submodule job – at e6cb1e8 (run 39750439670): 2362 passed, 872 skipped, 1 failed
tests/test_lane_worktrees_report.py 22 passed 22 passed at 210f4cf
tests/test_register_scan_hardening_138.py 29 passed 29 passed at 210f4cf
tests/test_lane_start_hardening_138.py 20 passed 20 passed at 210f4cf
tests/test_lane_start_claude_current.py 34 passed 34 passed at 210f4cf
tests/test_wrapper_hardening_138.py 24 passed 24 passed at 210f4cf
tests/test_skill_shell_blocks.py 6 passed 6 passed at 210f4cf
tests/test_install_skill_and_hook.py 73 passed 73 passed at 210f4cf
tests/test_openrepotools_command.py 97 passed 97 passed at 210f4cf
tests/test_triad_lane_tooling.py 5 passed, 22 skipped 5 passed, 22 skipped at 210f4cf
tests/test_triad_lane_tooling_handoff.py 1 passed, 21 skipped 1 passed, 21 skipped at 210f4cf
tests/test_a23_lane_start_read.py 10 passed 10 passed at 210f4cf
tests/test_lane_worktrees.py 118 passed 118 passed at 210f4cf

Each run was one pytest process per file, with every LANES_* and TMUX* unset, GH_TOKEN=bogus, a stub gh first on PATH and GIT_SSH_COMMAND=false. The "before" column is slice 1b's 6ecfc52, this branch's base, before any slice-4 commit. The lane modules and test_guard_launch_mode.py ran on a git archive of it. test_repo_hygiene.py and test_test_roots.py ran in a git worktree at it, because they read git's index and an archive has none. The "after" column is this branch's head 210f4cf, which also carries slice 1b's d9ff4e6; the new module, hygiene, test_guard_launch_mode.py, test_test_roots.py and test_precision_90a_96.py ran again at the fix round's head cd060f0 and again at the head of rv4-c3's round, e6cb1e8, and at the head of rv4-c4's round, 53b7bf3; the new module and hygiene ran again at this round's head, 1f16bbc, on the code leg, one pytest process each. The other rows were not run again locally after 210f4cf, because the rounds change only the sweep's own section of lane-end, its usage text and its cleanup, which acts only when the sweep holds a lock; CI's tests job runs every one of them. The merges changed neither lane-end nor the new module. The table holds every Python module under tests/ that names lane-end at the head (git grep -l), plus test_guard_launch_mode.py. The shell suite tests/test_lane_helpers.sh names it too, and is not run locally (below).

Not run locally, by the lane's rules: the shell suite (tests/test_lane_helpers_suite.py) and tests/test_supervised_restart_regressions.py. CI's tests job runs them. CI's one failure in both jobs is tests/test_claude_current.py::test_a_command_that_ignores_term_is_killed_after_the_grace[host] ("KILL never came: 30.0s"), the 26.04 image's timeout, which fails on every branch of 1b's line; package fails on the image's missing py3.9. 53b7bf3 adds only D31's reason text and D32's test parameter over e6cb1e8.

Hygiene before the merge of slice 1b'. At each commit of this round before e402a23, and at the re-homed head e5063e0, test_the_root_carries_the_line_ending_rule failed (234 passed, 18 skipped, 1 failed): it reads the root's .gitattributes, which the code leg's main has and which came here with slice 1b's code-leg branch. At 1f16bbc it passes. CI on the code leg is asked for with this description.

Mutants

Each mutant changes ONE line of lane-end's sweep (a check turned off, or a branch or message swapped) and runs the case or cases written to catch it: pytest -q -x in a git worktree, whose lane-end and test module were compared byte for byte with the branch's before every mutant. KILLED means pytest ended with 1 and a failing test; a collection error or any other end is not a kill. 144 mutants: 144 KILLED, 0 SURVIVED, 0 ERROR.

  • At 1f16bbc, on the code leg, all 144 in one battery, after the whole module passed there, each mutated line shown first to occur exactly once in lane-end. They are the 56 of the earlier rounds; the 55 of lane openRepoTools-3's round at 53b7bf3 (351, 352, 356, 371-374, 378, its re-check's 391 and 392, finding 545, D37 and the NITs; 111 at 811c10f, where c3-3-crlf alone survived, decision 25); and rv4b's round: MUST A's six, SHOULD D's seven, SHOULD B's five, SHOULD C's two and one port, the 353 gap's four and two more (decision 27), NITs G, I and J's four, and decision 30's two.
  • Where a round moved a mutant's line, the mutant was pointed at the new line, undoing the same behaviour: c3-3-crlf (decision 27), l3-359-colon (NIT J moved its block), l3-353-text (353's "a target that is gone is dropped", now inside sc_real_or_text), and the four of the .git/lanes-edit/ filter's find line (l3-351-scratch, l3-391-prefix, l3-392-kind, l3-392-error), to which decision 30 added a name.
  • The mutants were rebuilt for this round from the writers' records, since the files that held them were lost with a container's /tmp; each one's lines and cases are as last run on the old branch.

The table, one row per mutant (the line changed, the first failing case, and what the kill rests on), is in this PR's first comment: with 144 rows it does not fit in the description's 65,536 characters beside the rest.

In the table, "First failing case" is where pytest stopped, and the last column says "behaviour" where the hold is gone and the checkout becomes removable (removed under --yes). Where a kill rests on something short of a behaviour, it says so in the last column, and here:

  • Reason only, with no behavioural kill to be had. In each, a second layer keeps the same path or stops the same run, so no input shows a difference but the reason text or the exit code:
    • n-live: lanes gives a LIVE lane no class (ENDED|RETIRED) lr_class=closed, LIVE) : in lanes-edit.sh), so the class check keeps every LIVE lane with the LIVE rule off. The case pins the reason exactly (lane X is LIVE: a live session record here names it, not is LIVE, not CLOSED (Amendment 19)), for a LIVE lane and for an ENDED lane a live session record names.
    • n-link: a link falls to the "a directory whose real path is ..., not the path it is listed at" keep. The case pins symbolic link.
    • n-w: a directory sharing W's git directory falls to the "git directory is ..., not its own .git" keep. The case pins W itself, the readers' copy.
    • p-lane: without LANES_LANE, set -u stops the run (1, LANES_LANE: unbound variable) before anything is removed. The case pins 2 and the message.
    • p-nofetch: the listing's own refusal of a W fetch that did not run stops the run (1, "Nothing was removed"). The case pins 2 and the message.
    • c4-1-runner: the mutant is the text before rv4-c4's fix. The same branch keeps the runner's former name either way, and nothing is removed differently, so only the reason says what changed. The case pins the whole row, the former name's and the runner's own checkout's.
  • Reason only since 353. 353 reads the working directories and the lock's pid line again after the move, and moves the checkout back when either holds it. With the first read off (h-cwd, R1-no-cwd-rescan, n4-owner), that second read still holds the checkout, so no input shows a difference but the reason: the cases pin the first read's words (a live process works in it, is no longer this sweep's), not "as it was moved aside; it was moved back".
  • d29-listing, reason since D37. Every candidate is now asked whether a live process's LANES_REPO names it, and the case's window started before the rename does: with D29 undone, the IDLE lane's former name goes from keep to hold, not to remove. The case pins the keep row.
  • One case's two parameters, one kill. c4-2-rm (no rm -f of the pid file) is killed by the pid file whose write fails, as on a full disk: the lock is left holding the empty pid. The case's other parameter, a lock directory that cannot be written in, passes against it (c4-2-rm-control, runs/mut4-control.log: SURVIVED, 1 passed), because no pid file is made for the rm -f to remove, which is what rv4-c4 found.
  • Reason in the first case, behaviour in the second. For n-runner and n-xrepo, -x stops at test_W_and_live_lane_never_candidates, which fails on the reason text (the class check keeps the checkout under another reason). The case written for each, run alone against the same mutant, fails because the CLOSED checkout becomes a candidate: test_a_closed_runner_keeps_its_own_checkout and test_a_closed_lanes_checkout_named_by_LANES_REPO_is_kept.
  • The exit code came first, with a behaviour behind it. d-dry (the mutated dry run removed a checkout), p-uuid and m1b-listing-off (the run went on to the act), w-nofetch (the NOTED line was refused), n-wlanes. For w-rc and R4-no-partial-exit the exit code is the behaviour.
  • Wording. s2-exit3 and s2-exit6 keep the exit code (1) and fail on the message that says the line was "NOT written" and hands out "Write the line by hand" for a line that landed or is kept in the checkout; n2-ign and n3-count test the report's note of ignored files and the summary's counts, which are what they change.

Not mutable to a failing test: sc_under_home (candidates come only from <W>-lanes/* and the clones beside W, so no input reaches its false branch). It was not run as a mutant.

  • c3-3-crlf, the battery's survivor at 811c10f (tr -d '\r' replaced by cat in the dependents scan's .git file read). NIT 353 kept a target that does not resolve as written, so a CR kept after an absolute <clone>/.git still named a path under the clone. Decision 25 pointed it at a relative CRLF line; decision 27 spells such a line from the clone, so it now kills only through a line whose last name is a link to the clone's git directory, which resolves only once the CR is gone.

Questions, answered, and one for lane openRepoTools-3

  1. The per-profile clones beside W: answered by Brett Heap, 2026-10-22T12:46:57Z: "yes, have the sweep delete the 139 clones". Built as decision 23 (D37), under lane openRepoTools-3's conditions on the ruling: 352's wider dependents scan first (decision 22); a hold for any clone a window's LANES_REPO names; every hold a lane's checkout has, 351's and 371's among them; and the unpublished content kept until its owner acts. The cases mirror the <W>-lanes/ ones: a profile chain removed leaves first, a held link keeping what it borrows from, each hold, a dry run that lists each clone with its hold or remove, the caller's own clone kept, and a repository of another origin never listed. Decision 2 is superseded. Nothing is removed on Eagle until the sweep ships, with 2b, 3 and the flip.
  2. A working directory that cannot be read: answered by lane openRepoTools-3, keep the rule as built (counted and said, holding nothing). Its two additions: naming the container the scan saw is built (NIT J, decision 29); a /proc/<pid>/fd scan is not, by decision 29, and is a limit: a process that holds a file of a checkout open, with its working directory elsewhere, is not seen.
  3. Open question 5: answered by lane openRepoTools-3, yes, independently of question 1, in its own PR. Not in this slice (decision 14).
  4. For lane openRepoTools-3: the dependents scan's cost under --yes (rv4b's SHOULD E, decision 28). One full scan took about 44 minutes on Eagle (W's parent 814 s, /tmp 1,755 s, $HOME 54 s, under load), and --yes runs one at the listing, one under each candidate's lock and one after each move: up to 279 scans, about 203 hours, for the 139 clones beside W. A scan under a lock also outlasts the lock's 10-minute stale time; the pid line read after the move then holds the checkout, so nothing is removed wrongly, but the sweep holds where it should remove. Should the sweep scan once under the sweep-wide lock, and then only re-check what can change cheaply (each candidate's own objects and .git mtimes, or the first scan's matches read again without a new find)? And does SHOULD C's depth 8 for $TMPDIR stand, at /tmp's 1,755 s?

Not changed

  • docs/README-lanes.md: the manual's sweep section is slice 5's. After the cutover a docs edit is an openRepoTools-spec change.
  • lane-worktrees: see question 3, its own PR.
  • lanes-edit.sh: the sweep calls it (lanes, canon-lane, last-session, workstation, log) and changes nothing in it.
  • LANES_GIT_TIMEOUT=0 still means a fetch with no bound (rv4's NIT 4). It means that for lanes-edit.sh's own network calls too (D16), and the sweep keeps the one budget the helper has.
  • rv4's NITs 1-5 are all done. rv4's sketch for SHOULD 4 offered a second fix, a /proc/<pid>/environ scan; decision 15 says why it was not taken.
  • rv4-c3's SHOULD c3-1 and NITs c3-2, c3-3 and c3-4 are all done. Its probe P2 removes the pid file at the fetch, after the line was written; under the fix that lock reads as another writer's and is left to it, so the module's case makes the sweep's own write fail instead (a mkdir first on PATH that leaves the lock directory read-only).
  • rv4-c4's NITs c4-1 and c4-2 are both done (decisions 16 and 8); it found every earlier item closed.
  • Lane openRepoTools-3's review at 53b7bf3: MUST 351, SHOULDs 352 and 371-374, finding 545, and the NITs 353-356, 358, 359, 375-379; its re-check of 351: SHOULD 391 and NITs 392 and 394. All are done (decisions 17-22 and 24, and the round's tests). 393 is Amendment 23 slice 1b: every writer fetches and fast-forwards under its lock or refuses with nothing written, pushes with no pull and reads origin back, a refused push replays the one edit, and commit with nothing staged is the publish act (#198) openRepoTools#307's.
  • rv4b's pre-review of 811c10f: MUST A (decision 26), SHOULDs B, C and D, the 353 gap (decision 27) and NITs F to J (decisions 25 and 29) are done, one commit each. SHOULD E is measured and asked as question 4, not redesigned (decision 28). Of rv4b's list of lines no case pinned, the .git file naming a path gone after the move is decision 27's fix, and $HOME's root and relative alternates lines are pinned by r4b-home-depth and r4b-rel-alt.
  • sc_lock_drop and the exit's cleanup still release a lock whose pid line cannot be read, as lanes-edit.sh's mutex_release does. Since NIT c3-2 that state follows only a steal caught between its mkdir and its printf, after a ten-minute age-out the sweep's refreshes prevent.

Refs opensoft/openRepoTools#198

🤖 Generated with Claude Code

Brett Heap and others added 30 commits October 21, 2026 13:30
…eckouts CLOSED lanes left under `<W>-lanes/`, and every clone beside W, in a dry run that removes nothing (#198, slice 4 stage 1)

Stage 1 of slice 4: the flag, its refusals, the candidate list and the dry run.

- `--sweep-checkouts` is its own act, answered before the lane derivation like
  `--retire-dormant`. It refuses a lane, every other act's flag, `--yes` with no
  `LANES_LANE`, `--yes` under `LANES_NO_FETCH=1` (S3 refuses a writer whose
  fetch did not run, rv1b's MUST 3), and a W that stands in a `*-lanes`
  directory (as `lanes_home_check` does: from a lane window every other lane's
  checkout would stand "beside W").
- A candidate is a clone of the register directly under `<W>-lanes/`, at its
  own real path with a `.git` directory of its own, whose lane
  `lanes --closed --all` classes `closed`, or a former name's checkout beside
  its new one (S1). Never W, a LIVE lane's checkout, the runner's own, the
  checkout a caller's `LANES_REPO` names, a link, a non-clone or a hidden entry.
- The clones beside W are listed and never candidates: this build removes
  nothing outside `<W>-lanes/` (the brief's rule, kept against S6.2's wider
  one until lane openRepoTools-3 rules).
- The dry run exits 3 when there is a candidate and 0 when there is none.
  `--yes` is refused in this commit; the holds and the removal are stage 2.

Tests: tests/test_a23_sweep_checkouts.py, 16 cases on `tests/a23_box.py`'s
RegisterBox, in #202's `ended` style.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit faac2e7b90cec371780ec785cb513e24f2b7b0b6)
…D lane's register checkout only when nothing holds it, deciding each one again under its own writers' lock after a fetch (#198, slice 4 stage 2)

Stage 2 of slice 4: the holds and the removal.

- The holds (S6.2 (a) to (c)), each reported with what it holds:
  - (a) a `+` under `git cherry` on any local branch or a detached HEAD
    (refs spelled in full), a merge commit origin lacks (which `git cherry`
    skips), any other local ref reaching what origin lacks, a stash, staged
    or modified files, index entries marked assume-unchanged or
    skip-worktree, untracked files anywhere, an operation in progress,
    unmerged entries, an `index.lock`, the checkout's own writers' lock, and
    no `origin/<branch>` to compare with. Ignored files hold nothing and are
    named.
  - (b) another repository (W, one beside it, or one under `<W>-lanes/`)
    whose alternates file names its objects, and linked worktrees.
  - (c) a live process with its working directory inside (`/proc/*/cwd`,
    or `lsof`; with neither, "cannot tell" holds it).
- `--yes`, one candidate at a time (rv1b's MUSTs 1-2): take its
  `lanes/.lanes-edit.lock` with `lanes-edit.sh`'s pid line, fetch it
  (`LANES_GIT`, `LANES_GIT_TIMEOUT`, an explicit refspec), ask the lane's
  state again (`lanes --fetch --closed --lane`), read every hold, what leans
  on it and the working directories again, and only then move it aside to
  `<W>-lanes/.<name>.removing-<UTC>-<pid>` and remove that. A removal that
  does not finish says so by path and exits 1.
- The dry run tags each candidate `remove` or `hold`, and lists what holds
  each clone beside W. Nothing outside `<W>-lanes/` is removed.

The NOTED line and the leaves-first order are stage 3.

Tests: tests/test_a23_sweep_checkouts.py, 37 cases (16 of stage 1, now
reading `remove`, and 21 new: the act, 14 holds, a dry-run-only hold, a
live cwd, the re-check under the lock, a lane resumed after the listing, a
failed fetch, and nothing beside W removed).

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 991d8c6c97ee443897b4e0fc4787f4275440ae4c)
… of closed lanes' checkouts leaves first, keeps every clone a held one borrows from, and records what it removed in one NOTED line through the runner's own checkout (#198, slice 4 stage 3)

Stage 3 of slice 4: the dependents order and the NOTED line.

- Leaves first (S6.2 (b), D21): the candidates are taken in dependency
  order, every clone before the clones it borrows objects from, by passes
  that each place every candidate no unplaced candidate leans on, in listed
  order. A cycle places nothing more, and what is left goes last, held by
  what leans on it. The order is a plan: `--yes` still reads what leans on
  each candidate again just before its removal, so a held dependent keeps
  its whole chain.
- What leans on a clone (D18): a repository whose alternates file names an
  object directory AT OR UNDER the clone, not only its own `.git/objects`,
  because removing the clone removes any store kept inside it.
- The NOTED line (D9, D15, D20): after the removals, and only when something
  was removed, ONE `lanes-edit.sh log NOTED lane:<runner>` line in the
  runner's own log, written through the runner's own checkout under S3 and
  S4, in a subshell with `LANES_NO_FETCH` unset. Its text is built only from
  names held to letters, digits, `.`, `_` and `-`, and names no host path.
  A line the writer refuses exits 1 with the exact command that writes it
  by hand.
- The session field (D19): `--yes` takes `LANES_SESSION`, or the lane's
  last recorded session, and refuses (2) before anything is removed when
  that is not a transcript uuid.

Tests: tests/test_a23_sweep_checkouts.py, 47 cases (37 of stages 1-2, one
of them now checking the runner's NOTED commit, and 10 new: leaves first
in one run, a held link of the chain three ways, the NOTED line through the
runner's checkout, nothing removed writes nothing, a refused NOTED line
exits 1 with a working by-hand command, a non-uuid session refused, a store
kept inside a clone held, and a directory sharing W's git directory kept as
W). The CHAIN lanes repoS4-5, 6 and 7 are ENDED rows and logs in the
fixture; their checkouts are made by the cases that need them.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 2140189e2c7850475a1135a9ac7b07bc2219fcca)
…LDs and NITs) into slice 4: git found no conflict, since slice 4 changes only `lane-end` and its own test module (#198)

Brings slice 1b's six commits after `6ecfc52`, this slice's base:
`c454247` (MUST 4: a refused push whose commit is no longer HEAD is not
replayed), `10169f3` (MUSTs 1 and 2: `claim` and `add-row` run again on
origin's new tip), `92fbe46` (SHOULD 1 and NIT 1: the manual and the
carry and no-pull rules), `3c6b240` (rv1b's second report), `6471fd1`
(MUST-N and SHOULD B) and `d9ff4e6` (SHOULD A and NITs 1-2). They change
`lanes-edit.sh`, `docs/README-lanes.md`, `tests/test_repo_hygiene.py`
and four `tests/test_a23_*`/`test_commit_push_*` modules. Slice 4 changes
none of them, and `git merge-tree` predicted this tree before the merge.

The meeting point was weighed, and nothing in slice 4 changes. The
sweep's NOTED line is a `lanes-edit.sh log` write, and slice 1b's new
code is on that write's path (`log_sync`, `checkout_sync`, `cp_replay`).
The sweep relies on none of the replay's details: it passes the write
the runner's lane and transcript uuid, leaves `LANES_NO_FETCH` unset so
S3's fetch runs, and treats any refusal as exit 1 with the command that
writes the line by hand. The new carry and no-pull rules read only
`lanes-edit.sh` and the manual, and `lane-end` holds none of their words.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 210f4cfdbad2e427fe41b3fd6025b5b03971a2bb)
…) into slice 4: git found no conflict, since slice 4 changes only `lane-end` and its own test module (#198)

Brings slice 1b's two commits after `d9ff4e6`, the head slice 4 last
merged: `c71621f` (SHOULD C: the replay's drop is a compare-and-swap of
the branch from this write's own commit, so a commit that lands in its
window is never dropped) and `e6ea8f8` (the shell suite's stage 4: every
case that pinned the pull, the capture, the carry or a race now pins what
a write does instead). They change `lanes-edit.sh`, `docs/README-lanes.md`,
`tests/test_a23_publish_replay.py` and `tests/test_lane_helpers.sh`. Slice
4 changes none of them, and `git merge-tree` predicted this tree before
the merge.

The meeting point was weighed again, and nothing in slice 4 changes. The
sweep's NOTED line is a `lanes-edit.sh log` write, and `c71621f` is on
that write's replay path. The sweep relies on none of the replay's
details: it passes the write the runner's lane and transcript uuid,
leaves `LANES_NO_FETCH` unset so S3's fetch runs, and treats any refusal
as exit 1 with the command that writes the line by hand. The shell suite
names no `--sweep-checkouts` case.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit cede05468222b3929d9d18fdf74e34f5670ab173)
…es on a register whose fetch of W failed, at the listing or at the re-ask under each checkout's lock (rv4's MUST 1, #198, slice 4)

`lanes --fetch` exits 0 on `log_sync`'s `fell-back` (ls-remote answered, the
fetch of W then failed or timed out) and reads the stale ref; it says so only
in its notes, with the phrase "reading the logs as they stand locally". The
sweep matched only "NO FETCH WAS MADE" at the listing and sent the re-ask's
stderr to /dev/null, so `--yes` took a stale register for "fetched just now"
and removed the checkout of a lane that had RESUMED on origin.

- The listing (`--yes`): the refusal matches the shared phrase, so a failed
  fetch of W refuses with 1 and "Nothing was removed", as a fetch never made
  does.
- `sc_still`: the re-ask's stderr is captured (into `LE_TMP`, which `cleanup`
  removes on every exit) and the checkout is held, with a reason that says
  the state was not read fresh, when it says "reading the logs as they stand
  locally", "session records could not be read" or "could not be read, so its
  rows are not in this listing". A capture that cannot be made or read back
  holds too.
- Tests (`tests/test_a23_sweep_checkouts.py`), each red on `cede054` and green
  here: a W fetch that fails at the listing refuses `--yes` and changes
  nothing (rv4's P5); a lane that RESUMES at its checkout's fetch while W's
  fetch fails from then on is kept (P1); session records that become
  unreadable at the re-check keep the checkout.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit b896230ddb1aab37138889d048c33582bc4ec713)
…tes cannot be read back holds that checkout by itself, with its own reason, and a register unreadable at the re-check gets a test of its own (rv4-r2's SHOULD 1, #198, slice 4)

`sc_still` captured the re-ask's notes in `LE_TMP` and, where `cat` could
not read the file back, put a stand-in sentence in their place that was
written to match the "could not be read, so its rows are not in this
listing" clause. So the checkout was held only through that clause, the
reason it printed was false ("the register could not be read"), and a
reword of the clause would have let the path fail open with no test to
notice.

- `sc_still`: a failed `cat` of the capture sets its own reason ("its
  capture could not be read back") and returns 1 at once, as the `mktemp`
  branch above it does, and as the listing refuses on both.
- Tests (`tests/test_a23_sweep_checkouts.py`):
  - the capture goes mode 000 during the re-ask's own fetch of W (rv4-r2's
    probe): held, with the capture's reason and not the register's. Red at
    `b896230` (it named the register), green here;
  - the register cannot be read at the re-check: origin takes a commit that
    changes it, the re-ask's fetch of W brings it in, and its blob is then
    moved out of W's objects, so the tree names a register `git show`
    cannot read. Held, "the register could not be read". It passes at
    `b896230` too, since the clause was there; it is the test that now
    reaches the clause, which the capture's stand-in reached before. With
    the clause deleted, both the CLOSED lane's checkout and the former
    name's are removed.
  - `_push_from_peer`, a hook command in which the peer that seeded origin
    appends a line to a file and pushes it.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 2e58431d2483147ebd9e74ea712892f95f8a380c)
…gin at its checkout's fetch share one hook command, `_resumed_on_origin`, instead of each spelling the peer's pull, append, commit and push (rv4-r2's NIT 2, #198, slice 4)

The re-check case from rv1b's MUSTs 1-2 and the one from rv4's MUST 1 (P1)
each carried the same five-step shell body, which Sonar's duplication check
may flag. `_resumed_on_origin(box, lane)` builds it on `_push_from_peer`,
and both cases call it. No case changes what it asserts.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e152a89daa1ab9824646165c23bb42083a99e2d9)
…NO FETCH IS MADE is pinned at the listing and at the re-ask under a checkout's lock, as the fetch that fails after `ls-remote` already was (rv4-r2's NIT 1, #198, slice 4)

The module tested only `log_sync`'s `fell-back` (the fetch fails after
`ls-remote` answers). `lanes --fetch` says the other way, an `ls-remote`
that fails, as "NO FETCH WAS MADE ... reading the logs as they stand
locally", and nothing here made it.

- The listing's refusal case is parametrized over the failing operation:
  the fetch (`fell-back`), and `ls-remote` (no fetch). Each refuses `--yes`
  with 1 and changes nothing.
- A new case: W's `ls-remote` fails from the candidate's own fetch on, so the
  re-ask makes no fetch, and the checkout is held as "not read fresh ...
  W's fetch did not complete".
- With the listing's refusal disabled both listing cases fail, and with
  `sc_still`'s fetch clause disabled the new case fails (and so does P1).

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit dfec3c5c03f2a170e2aa2e45f42a736826de8be5)
… or 6 (its commit kept) is no longer reported as "NOT written" with a by-hand command that would record the sweep twice; both are handed the publish act first (rv4's SHOULD 2, #198, slice 4)

`lanes-edit.sh log`'s 3 means the line was committed and pushed and the
read-back could not confirm it: it may be on origin. Its 6 means, in most of
its causes, that the commit is kept in the checkout the write went through
and only origin lacks it (a push refused with origin unmoved, the replay's
bound), and in the rest that `git add` or `commit` failed with nothing
committed. The sweep said "NOT written" for every non-zero exit and handed
out `log NOTED ...` by hand, which then wrote a second line, or met S3's
refusal of a checkout holding a commit origin lacks.

- 3: the sweep says the line may be on origin already and is not to be
  written again, and prints the publish act (`LANES_LANE=<runner>
  lanes-edit.sh commit "..."` with nothing staged), which sends the commit
  only where origin lacks it by content and says so where it does not.
- 6: the publish act first; the by-hand line only where the publish act
  finds nothing to send, which is the `git add`/`commit` failure.
- Any other refusal: as before, "NOT written" and the line by hand.
- Each remedy is printed on a line of its own. The exit is still 1.
- Tests (`tests/test_a23_sweep_checkouts.py`), both red at `dfec3c5` ("was
  NOT written") and green here:
  - rv4's P2: the real `log` runs and its wrapper exits 3; origin holds the
    line once, the sweep hands out the publish act alone, and running it
    leaves origin holding the line once;
  - every push is refused with origin unmoved, so `log` exits 6 with its
    commit kept on HEAD; the sweep hands out the publish act and then the
    by-hand line; with pushes allowed again the publish act sends the kept
    commit, and origin holds the line once.
  - The refusing-log case now uses the shared `_wrapped_log` and `_by_hand`
    helpers; it asserts what it did.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit a4cbf1a0d08d16cdee213c0b4a5b05477d5222ca)
…r each checkout's lock, and the exit for a removal that did not finish, each get a case, so none of rv4's four one-line mutants survives (rv4's SHOULD 1, #198, slice 4)

rv4 found four one-line mutants of `lane-end` that no case killed. Each is a
part of D14 (the decision made again under the lock) or an exit:

- R1, the cwd scan not repeated under the lock: a process starts working in
  the CLOSED lane's checkout at that checkout's own fetch (rv4's P4), and the
  checkout is held, naming its pid. Under R1 it is removed.
- R2, the listing's refusal on a W fetch that did not complete: killed since
  `dfec3c5` by both cases of the parametrized listing test (the fetch fails
  after `ls-remote`, and `ls-remote` fails).
- R3, the former-name re-ask never holding: the listing finds repoS4-8's
  checkout beside its new name's, and at its own fetch the new name's
  checkout goes. The former name's checkout is then that lane's own again
  (S1), and is held as "no longer a former name". Under R3 it is removed.
- R4, no exit 1 for a removal that did not finish: the CLOSED lane's
  checkout holds an ignored, read-only directory, so its aside cannot all be
  removed. It is named PARTLY, the run exits 1 naming the aside, and the
  aside stays. Under R4 the run exits 0. Skipped as root.

Each was run once against its case in a separate clone (`c7-mut.py`): all
three KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 9a7e8c88b544a0e3a0cdab17fc29f8444524de13)
… assert per condition, so a failure names the condition that failed (rv4's SHOULD 3, Sonar S9073, #198, slice 4)

The defect slice 1b split out in `3c6b240`. `assert a and b` reports only
that the whole was false; each condition is now its own assert, with the
message the composite carried where it had one. The refusal cases (nothing
under `<W>-lanes/` changed, origin unmoved), the beside-W listing, every
hold, the fetch that fails, the NOTED line's fetch-before-push order,
nothing removed writing nothing, the store kept inside a clone, and the
directory sharing W's git directory. No case changes what it asserts.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 1345897d7bb9dcc64475ebc8616e3390607fb7cc)
… LIVE, at the listing and in the re-ask under its lock, since a window started before the rename may still write through it (rv4's SHOULD 4, D27, #198, slice 4)

Since 10-19 the launcher exports `LANES_REPO=<W>-lanes/<lane>`, and a window
of a lane started before its rename still names the old path. The sweep
took a former name's checkout beside its new one as a candidate whatever
the lane's state, and only its own caller's `LANES_REPO` was kept, so a
LIVE window's writes failed after the sweep until it was relaunched.
Nothing was lost (the holds pass), but the window broke.

- `sc_judge`: a former name's checkout beside its new one is kept while
  that lane is LIVE ("a former name of lane X, which is LIVE: a window of
  it started before the rename may still write through this path, which
  its LANES_REPO names").
- `sc_still`: the former-name re-ask holds it when the lane reads LIVE now.
- D27: LIVE, and not `/proc/<pid>/environ`. On this workstation a window of
  the lane is a live session record naming it, which is what LIVE is, and
  the listing and the sweep already share that read. `environ` is the
  environment at exec only, cannot be read for another user's processes,
  and does not exist on macOS. A renamed lane's old checkout now stays
  while the lane is LIVE; the next sweep after that removes it.
- Tests, both red at `1345897` (the former name's checkout is `remove`, and
  removed) and green here: repoS4-9 LIVE (a live session record names its
  transcript) keeps repoS4-8's checkout in the dry run and under `--yes`;
  repoS4-9 coming LIVE at repoS4-8's own fetch holds it as "LIVE now".

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 65594885b445802224064c5e2dcba54bca4425a9)
… it, the act names ignored files and counts a partial removal apart, and under each lock the sweep keeps its lock fresh, leaves one taken over, and asks again what made the checkout a candidate (rv4's NITs 1-4, #198, slice 4)

- NIT 1: `sc_dep_scan` reads the `.git` FILES in the directories it reads
  alternates files in (W, the clones beside it, `<W>-lanes/`), and a clone
  whose git directory, or one inside it, such a file names is held: "its
  git directory is named by the .git file of X, which works in it" (rv4's
  P3). `sc_dependents` takes the list to read as an argument.
- NIT 2: the act's `removed` and `PARTLY` lines name the ignored files, as
  the dry run's `remove` line does, so an ignored nested repository that
  goes with a checkout is on the record.
- NIT 3: a removal that did not finish is counted apart: "N removed, M moved
  aside and not all removed, K held", and the NOTED line's "removed N"
  counts only finished removals. A run whose only act is a partial removal
  still writes its NOTED line.
- NIT 4, under each checkout's lock:
  - the lock's age is refreshed (`touch -c`, which never makes a lock)
    before the fetch and before the re-ask, as `git_net`'s heartbeat does;
  - `sc_is_register_clone` is asked again after the re-ask, so a checkout
    that stopped being a clone of the register's origin is held;
  - the lock's pid line is read again just before the move, and a line
    naming another holder (a takeover) holds the checkout and leaves that
    lock to its holder; `sc_lock_drop` and `cleanup` leave such a lock, as
    `mutex_release` does.
  - `LANES_GIT_TIMEOUT=0` still means no bound, as it does for
    `lanes-edit.sh`'s own network calls (D16): left on purpose.
- EXIT 1's text names a removal that did not finish and a NOTED line not
  confirmed.
- Tests, each red at `6559488` and green here: a `.git` file beside W
  naming the CLOSED lane's `.git` (held, dry run and `--yes`); an ignored
  file named on the `removed` line; the partial-removal case now also pins
  "1 removed, 1 moved aside and not all removed" and the NOTED line's
  counts; a checkout whose origin is re-pointed at its own fetch (held); a
  lock whose pid line another writer replaced at the fetch (held, and that
  pid line left as it is).

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 10468ac67729a573ec0712eb7ca0cd6628b6abfb)
…ved are one parametrized case, which adds D19's refusal when `LANES_SESSION` is unset and `last-session` names no session (rv4's NIT 5, #198, slice 4)

Three near-identical cases (no `LANES_LANE`, `LANES_NO_FETCH=1`, a
`LANES_SESSION` that is no uuid) each asserted rc 2, their sentence, and
nothing under `<W>-lanes/` or on origin changed. They are one case,
`test_yes_is_refused_before_anything_is_removed`, which also asserts
"Nothing was removed" for all of them. A fourth parameter is D19's other
branch, untested until now: `LANES_SESSION` unset and a `last-session` that
names none (a wrapper exits 8), refused naming both. With D19's uuid check
disabled (`p-uuid`), both session cases fail. The wrapper helper takes the
verb it stands in for (`_wrapped`).

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit a376e5b0bb67242a6212822b5e05408c57d435be)
…eason, and the runner's own checkout and the one a caller's `LANES_REPO` names are kept even where their lane is CLOSED, so the battery's three mutants the module missed are killed (#198, slice 4)

The 31-mutant battery at `cede054` killed `n-runner` and `n-xrepo` by their
keep reason alone (the fixture's runner is IDLE, and its `LANES_REPO` names
the PAUSED lane's checkout, so the class check kept both), and `n-live`
survived.

- The runner is the CLOSED lane: its own checkout is kept as the runner's,
  and only the former name's checkout is swept, by the dry run and by
  `--yes`. Under `n-runner` that checkout is a candidate.
- `LANES_REPO` names the CLOSED lane's checkout: kept as `LANES_REPO`'s.
  Under `n-xrepo` it is a candidate.
- A LIVE lane, and an ENDED lane whose transcript a live session record
  here names: each kept with "lane X is LIVE: a live session record here
  names it", dry run and `--yes`. No case can kill `n-live` by behaviour:
  `lanes` gives a LIVE lane no class at all (`lanes-edit.sh`:
  `ENDED|RETIRED) lr_class=closed`, `LIVE) :`), so no listing has a lane
  that is LIVE and classed closed, and the class check keeps every LIVE
  lane too. The LIVE rule is defence in depth, and the reason is pinned.

Each of the three mutants was run once against these cases in a separate
clone: all KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 459c1f6b2a696bc20885558db04c4a00378e1a9b)
…sc_lock_drop`, when something else holds the checkout first, and by `cleanup`, when a signal ends the sweep (rv4's NIT 4, #198, slice 4)

`10468ac` made `sc_lock_drop` and `cleanup` leave a lock whose pid line
names another holder, as `mutex_release` does, but its case reached
neither: the check before the move clears the sweep's hold on the lock
first. Two cases now reach them:

- the takeover case is parametrized: the pid line is replaced at the
  checkout's fetch and that fetch then fails, so the checkout is held for
  the fetch and `sc_lock_drop` runs; the other writer's pid line is left;
- a new case: the pid line is replaced at the fetch and the sweep is sent
  TERM; it exits 143, and `cleanup` on the way out leaves that pid line and
  the checkout.

With either ownership check made unconditional, its case fails.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit cd060f09a9644397be53e8c3e6169e97f45b72b0)
… read and the manual's wording) into slice 4: git found no conflict, since slice 4 changes only `lane-end` and its own test module (#198)

Brings slice 1b's twelve commits after `e6ea8f8`, the head slice 4 last
merged, among them: `9cdf8ab` and `4b94f87` (A37: `CP_KEEP_IF_GONE` names
the one file a write edits in place, so a refused push whose origin no
longer has it keeps the commit on HEAD instead of replaying into nothing,
and the replay's HEAD check has a case only it can answer), `638b13a` and
`f9b1de9` (the refusal with origin unmoved, and the unconfirmed push, read
HEAD before they say "kept on HEAD"), `3a4bc4a` (a drop's compare-and-swap
refused for a held `refs/heads/<b>.lock` shows git's own words), `514b0c4`,
`251e900` and `9683fd1` (`migrate-state-cells`' lines for a case-only
rename), `24efc8a` and `0c822f3` (cases for the A37 check and the carry
form's tree step), and `5588307` (the manual's words for `add-row`'s
refusal). They change `lanes-edit.sh`, `docs/README-lanes.md`,
`tests/test_a23_publish_replay.py` and `tests/test_a23_guard_refusals.py`.
Slice 4 changes none of them, and `git merge-tree` predicted this tree
(cb4640e) before the merge.

The meeting point was weighed again, and nothing in slice 4 changes. The
sweep's NOTED line is a `lanes-edit.sh log` write, and the refusals this
round reworded are on that write's publish path. The sweep relies on none
of their wording: it passes the write the runner's lane and transcript
uuid, leaves `LANES_NO_FETCH` unset so S3's fetch runs, and treats any
refusal as exit 1 with the publish act or the command that writes the line
by hand. `lane-end` and `tests/test_a23_sweep_checkouts.py` are byte-identical
to `cd060f0`'s, so the mutant battery run at `cd060f0` still holds.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 4f3108449919dd4ac83822b41a72d858aa742a62)
…its lane is CLOSED, as every other checkout is, at the listing and in the re-ask under its lock, since a process started before the rename may still write through it (rv4-c3's SHOULD c3-1, D29, #198, slice 4)

D27 kept a former name's checkout beside its new one only while its lane
was LIVE. LIVE is a pid test in this container, so it missed two things
that still carry the old `LANES_REPO=<W>-lanes/<old>`: a process started
from a window before the rename (a landing pilot) that outlives that
window's session, and a pre-rename window in another container that
shares `<W>-lanes/`. Either way the lane reads PAUSED or IDLE, the old
checkout was removed, and that process's next register write (a HOLD's
release, say) was refused by S3 as "not a git checkout", so an open HOLD
blocked every landing. Nothing was lost, since the holds passed.

- D29 takes rv4-c3's first option and supersedes D27's LIVE test:
  `sc_judge` judges a former name's checkout beside its new one through
  `sc_judge_lane`, as the lane's own checkout is (the runner's lane kept,
  a lane the register does not know kept, LIVE kept, anything but CLOSED
  kept, CLOSED a candidate). The keep line says first that it is "a
  former name of lane X, beside that lane's own checkout, kept until that
  lane is CLOSED: a process started before the rename may still write
  through this path"; the candidate line names the new checkout and
  "lane X is CLOSED (<state>)".
- `sc_still`: the former-name re-ask under the lock no longer returns
  after its rename checks; it falls through to the LIVE and CLOSED checks
  every candidate gets, so a lane that RESUMES or comes LIVE after the
  listing holds the checkout.
- The cost: a renamed lane's old checkout stays until the lane closes.
- Tests, red at `4f31084` (17 of 19 cases; the former name is `remove` or
  `removed`) and green here (19 passed):
  - new: rv4-c3's P1 (a `sleep` carrying `LANES_REPO=<W>-lanes/repoS4-8`,
    lane repoS4-9 IDLE: kept by the dry run and `--yes`); a former name
    of a CLOSED lane is removed, with its lane's own checkout; a former
    name whose lane RESUMES at its fetch is held "IDLE now, not CLOSED";
  - the D27 cases read D29's reason, and the one that comes LIVE after
    the listing starts from a CLOSED lane;
  - the cases that counted repoS4-8's checkout as removable by default
    now count repoS4-2's alone, or a CHAIN lane's checkout where they
    need a second candidate (`_closed_checkout`); `_new_name_ended` ends
    repoS4-9 on origin and in W.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 67e3433a30f07283d1090a07e8d3edf423ee2699)
…writers' lock it just took lets that lock go and holds the checkout, so it no longer leaves a lock that names no holder, with a false "taken over" reason (rv4-c3's NIT c3-2, #198, slice 4)

`sc_lock_take` wrote its pid line with `|| :`. When the line was never
written (a full disk, a directory that cannot be written in), the check
before the move read an empty line, held the checkout as "a writer took
it over", and cleared `SC_LOCK`, so the empty lock directory the sweep
had made stayed. Every later sweep then held that CLOSED lane's checkout
for it, and its writers waited out `mutex_stale`'s ten minutes.

- `sc_lock_take`: a pid line that cannot be written empties and removes
  the lock just made, and holds the checkout: "its writers' lock was
  taken, and this sweep's pid line could not be written into it, so the
  lock was let go again" (or, where `rmdir` fails too, that the lock is
  left). No writer can have taken a lock from a live holder in under ten
  minutes, so the lock is still the sweep's to remove. An empty line at
  the check before the move is then only ever another writer's.
- Test (red at the parent: the lock is left; green here):
  `test_a_lock_whose_pid_line_cannot_be_written_is_let_go`. A `mkdir`
  first on PATH leaves the new lock directory read-only (mode 500), so
  the sweep's own `printf` fails, as rv4-c3's P2 has it. P2 itself took
  the pid file away at the fetch, after the line was written; under this
  fix that lock reads as another writer's and is left to it, as the
  takeover case already pins, so the module makes the write itself fail.
  Skipped as root.
- Not changed: `sc_lock_drop` and `cleanup` still release a lock whose pid
  line cannot be read, as `lanes-edit.sh`'s `mutex_release` does.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 95f62f1d830e1770a6beed1ebce80e423fa93cc8)
… read as git reads it, so a directory working in a clone's git directory through one still holds that clone (rv4-c3's NIT c3-3, #198, slice 4)

`sc_dep_scan` took the `gitdir:` line with `sed`, which kept the line's
trailing CR, so the path it named did not resolve and the directory was
not seen. git strips the CR and works in the clone's git directory, so
the dry run listed that clone as `remove`, and `--yes` would have removed
the git directory another checkout works in.

- `sc_dep_scan`: `tr -d '\r'` strips the CR before `sed` reads the line.
  The `\r` is `tr`'s, not `sed`'s, because BSD `sed` reads no `\r` in its
  script.
- Test: `test_a_git_file_naming_a_clones_git_directory_holds_it` is now
  parametrized over how the `.git` file spells the path. The new `CRLF`
  case (rv4-c3's P3) is red at the parent (`remove` for `hold`) and green
  here, as is the `absolute` case. `git rev-parse --absolute-git-dir`
  proves the file works in the clone's git directory first.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5b029c471f8854879a0ded51bec6106fb26197b5)
…t, and a `.git` file whose `gitdir:` is relative to the directory it is in, each get a module case, so neither branch can be undone unseen (rv4-c3's NIT c3-4, #198, slice 4)

rv4-c3 found two branches of the last fix round with no module case and
no mutant: `sc_lock_beat`, which refreshes the lock's age before the
candidate's fetch and before the re-ask, and `sc_dep_scan`'s reading of a
relative `gitdir:` against the directory its `.git` file is in. Both
behave at the head (rv4-c3's P5 and P4 pass there); nothing pinned them.

- `test_the_lock_is_refreshed_before_the_reask` (P5): the candidate's own
  fetch ages its lock twenty minutes (`TZ=UTC0 touch -t`, POSIX, where
  the probe had GNU's `touch -d`), and a `lanes` wrapper finds it under
  ten minutes old (`find -mmin -10`, as `mutex_stale` ages it) at the
  re-ask. The case also checks that the hook did age it.
- `test_a_git_file_naming_a_clones_git_directory_holds_it[relative to the
  directory it is in]` (P4): the third spelling of the `.git` file case.
- Each is shown to bite by the mutant that undoes its branch, run once in
  the mutant clone: `sc_lock_beat`'s body emptied (`sc_lock_beat() { :; }`)
  and the relative branch dropped. Both KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e6cb1e8a7926c554c004d2a645ba17da2534312b)
… kept for its own true reason, not "until that lane is CLOSED" (rv4-c4's NIT c4-1, #198, slice 4)

rv4-c4 found one thing wrong in the last round's reasons. Where lane
repoS4-8 is renamed repoS4-9 and repoS4-9 runs the sweep, the sweep
kept repoS4-8's checkout with the former-name prefix of D29 in front
of the runner's reason: "... kept until that lane is CLOSED: ...; lane
repoS4-9 runs this sweep: its checkout is the one its NOTED line goes
through". Both halves are wrong for that checkout. The lane may be
CLOSED already, and the NOTED line goes through repoS4-9's own checkout,
not this one.

`sc_judge_lane`'s runner branch now says, for a former name (its third
argument, the new name's checkout beside it): "a former name of lane X,
which runs this sweep: a process it started before the rename may still
write through this path". That is what the checkout is kept for: a
process started before the rename, whose LANES_REPO names the old path
(D29). The branch for the runner's own checkout is unchanged. Nothing is
kept or removed differently, only the reason said.

`test_a_former_name_of_the_runners_own_lane_is_kept_for_its_own_reason`
(rv4-c4's P6, as a module case, the dry run and `--yes`): repoS4-9 ENDS
and runs the sweep; the former name's row and repoS4-9's own are asserted
whole, repoS4-2's is the only checkout swept, and both stay on disk. Red
at e6cb1e8 (the "kept until that lane is CLOSED" text), green with the
fix.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 78caf60bedc19501f6af8d7d72a201b8ec17f9dc)
…rite the way a full disk does, so the `rm -f` of the pid file is pinned (rv4-c4's NIT c4-2, #198, slice 4; tests only)

rv4-c4 found `sc_lock_take`'s `rm -f` of the pid file unpinned. The
module's case for a pid line that cannot be written made the lock
directory mode 500, so the open of the pid file failed and no pid file
was ever made: nothing for the `rm -f` to remove. On a full disk the
open succeeds and leaves an EMPTY pid file, and then the `rmdir` of the
lock fails unless the sweep has removed that file, so the lock is left
(c3-2's harm). rv4-c4's probes showed it: the module's case passes
against a lane-end without the `rm -f` (P8m), and a case with a link to
/dev/full as the pid file fails against it, the lock left holding
['pid'] (P7m).

`test_a_lock_whose_pid_line_cannot_be_written_is_let_go` takes a second
parameter, rv4-c4's P7 (`ln -s /dev/full <lock>/pid` after the `mkdir`
shim's own `mkdir`): the pid file's write fails with ENOSPC. The first
parameter is the case as it was. The case's assertions are the same for
both: the checkout is held with the lock-was-let-go reason, the lock is
gone, and the next sweep removes the checkout. The new parameter skips
where there is no /dev/full; the first skips as root, as it did.

The code is right at e6cb1e8, so the new parameter passes there. It
bites against the mutant that drops the `rm -f` (`c4-2-rm`): KILLED,
the lock left holding the pid file.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 53b7bf3349dfa94fc01dd52e830a0f94f90364bb)
…ept in `.git/lanes-edit/` holds the checkout, so the sweep no longer deletes a person's only copy of it (lane openRepoTools-3's MUST 351, #198, slice 4)

S3 step 4's carry saves a hand edit of the register to
`.git/lanes-edit/hand-edit-<UTC>-<pid>.diff`. Where the edit no longer
applies to origin's version, `commit` refuses, KEEPS the diff there and
leaves the register at origin's version, so `git status` and the
untracked list see nothing. `sc_holds` read nothing in the git directory
but the operation markers, `index.lock` and `worktrees/`, so lane 3's
reproduction ended with the CLOSED checkout and the kept diff both gone,
and a NOTED line saying it "held nothing unpublished".

- `sc_holds`: anything in `.git/lanes-edit/` holds, but the writer's own
  scratch: git's words from a write (`apply.err`, `fetch.err`, `ff.err`,
  `reset.err`) and the lines a carry removed (`removed`). It says
  "lanes-edit.sh keeps N (<names>) in .git/lanes-edit/: a hand edit of the
  register its commit could not carry, or the copy of one it carried
  (read it; remove it once origin has the edit)". A `.git/lanes-edit/`
  that cannot be listed holds as well, with its own reason.
- This is lane 3's tested fix, with `reset.err` added to the scratch: the
  push replay's drop writes it there (`lanes-edit.sh:3033` here, `:3161`
  at fc6f34d), so every checkout whose write once lost a push race would
  otherwise be held for good.
- Tests (red at 53b7bf3: `remove` for `hold`; green here):
  `test_a_hand_edit_lanes_edit_kept_holds`, end to end: the row is edited
  by hand in the CLOSED lane's checkout, a peer changes the same row on
  origin, and the real `lanes-edit.sh commit` exits 2 and keeps the diff;
  the dry run and `--yes` both hold the checkout and name the file, and
  both stay. Its second parameter makes the directory mode 000 (skipped
  as root). `test_lanes_edit_scratch_alone_holds_nothing` is the control:
  the five scratch names alone hold nothing, and the checkout is removed.
- Not changed: the copy a successful carry leaves is held too, until a
  person removes it. Lane 3 gives that companion to #307 (delete or
  rename the copy once the commit is confirmed on origin).

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e78e9fd81213ff161f40a79b589390e4454f3b2c)
…or under it is held, and the removal never crosses a mount, so `rm -rf` no longer deletes what a mount shows from outside `<W>-lanes/` (lane openRepoTools-3's SHOULD 371 and 357, #198, slice 4)

A mount point at an ignored path, or inside `.git/`, held nothing: ignored
files hold nothing, and nothing reads `.git/` for mounts. The `mv` then
carried the mount along with its directory, and `rm -rf --` of the aside
deleted what the mount showed, which lives outside `<W>-lanes/` (lane 3
showed it with the sweep's own two commands in a privileged container).

- `sc_mount_scan` reads the mount table: `/proc/self/mountinfo`'s field 5,
  or, where there is no such file, `mount`'s output (`X on DIR [type T]
  (opts)`, Linux's and BSD's). A table that cannot be read, or that names
  no `/`, is no mount table: the sweep cannot tell, and holds every
  candidate with "whether a file system is mounted in it cannot be told".
- `sc_mounts_inside` spells the checkout's path with mountinfo's octal
  escapes (backslash, space, TAB) and matches it, through ENVIRON, as the
  mount point itself or a prefix with `/`. `sc_judge_holds` holds with "a
  file system is mounted at or under it: N (...), and removing it would
  remove what that mount shows, which is not this checkout's".
- It is read at the listing, with the working directories, and again
  under each candidate's lock with the dependents and the working
  directories, just before the move.
- A second layer only: `rm -rf --one-file-system` (GNU) or `-x` (BSD),
  each asked once of a path that is not there; an `rm` that takes neither
  removes as before. Alone it is not enough: a bind mount of the same file
  system is one `rm` cannot see.
- `LANE_END_MOUNTINFO` names another table: the test seam, in the
  ENVIRONMENT block.
- Tests (red at e78e9fd: `remove`/`removed` for `hold`; green here):
  `test_a_mount_inside_a_checkout_holds` (at an ignored path, inside
  `.git`, at a path with a space, which mountinfo spells `\040`), the dry
  run and `--yes`; `test_a_mount_made_after_the_listing_holds` (the table
  names the mount only from the checkout's own fetch on: the re-read under
  the lock holds it); `test_a_mount_table_that_cannot_be_read_holds` (no
  table and a failing `mount`; a table naming no `/`).
- Not run: a real bind mount through `lane-end` end to end. This host
  has no user namespaces (`unshare -rm`: "Operation not permitted"), so
  the tests read a table through the seam.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 50a37b7c584ce6576feac02b3f15d94666629308)
…atched against the mount table as `/proc/self/mountinfo` spells it, so 371's escaping is pinned (lane openRepoTools-3's SHOULD 371, #198, slice 4; tests only)

The escape mutant (the checkout's path matched unescaped) survived 371's
cases: in each, the space was in the MOUNT POINT under the checkout, and
the checkout's own path, the prefix that is matched, held none.

- `test_a_mount_in_a_checkout_whose_own_path_has_a_space_holds`: a box
  whose W stands in `with space/`, and a mount table naming a mount point
  at an ignored path in the CLOSED lane's checkout, as mountinfo spells
  it (`with\040space`). The dry run holds the checkout and names the
  mount point. With the escaping undone it is `remove`.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 596aeeeb32732fb274d5fdd855d8c95061c7e4a7)
…n hand finish and prints its NOTED line by hand, and every run names an unfinished removal's aside with the command that removes it (lane openRepoTools-3's SHOULD 372, #198, slice 4)

- INT and TERM wait while a checkout is moved aside, removed and counted:
  the `mv` and the `rm` run in subshells that ignore both (`exec` keeps an
  ignored signal ignored), so a Ctrl-C to the process group no longer stops
  `rm` halfway and leaves a whole checkout under a hidden name.
- A signal then ends the act. After any removal it prints the NOTED line
  by hand for what was done, ending "the sweep was stopped by a signal".
  One that comes once the NOTED write has started says the line may be on
  origin already and hands out the publish act first.
- Every run lists each `.<name>.removing-*` under `<W>-lanes/` as a
  `leftover` line, apart from the hidden count, with
  `chmod -R u+rwX -- <p> && rm -rf -- <p>`. No sweep removes one.
- A removal that did not finish is said before the NOTED write, so a
  refused write no longer exits before saying it.

Tests: five new cases. All five fail at 596aeee and pass with the fix, with
their neighbours (11 passed). Hygiene: 253 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit ff12b0154850bf0b0bf823e29f1100fa813b945d)
…t its own cause needs (lane openRepoTools-3's SHOULD 373)

Exit 6 of `lanes-edit.sh log` has three causes, and the two remedies the
sweep printed for every 6 (the publish act, then the line by hand) both
refused for two of them. The write's words are now kept (captured inside
the write's own subshell, so a signal's trap still speaks on the sweep's
stderr) as well as shown, and choose the remedy:

- a push refused with origin unmoved: the line is committed and only
  origin lacks it, so the publish act alone;
- `git add failed` / `git commit failed`: the line is staged or in the
  tree, so `git -C <checkout> restore --staged --worktree --
  lanes/log/<runner>.md`, then the line by hand;
- the bound ("the commit is local and kept on HEAD", or no commit to drop
  to): S6.3's archive, `git -C <checkout> push origin
  HEAD:refs/heads/archive/<runner>-stranded-<UTC>`, then `reset --keep
  refs/remotes/origin/<branch>`, then the line by hand;
- words the sweep does not know: the two remedies as before.

The checkout is the one `lanes-edit.sh` writes through (S2): the caller's
LANES_REPO, the runner's own under LANES_ROUTE_WRITES=1, else W.

Tests: a pre-commit hook in the runner's checkout, and a peer commit landing
before each of the six pushes; each case runs the printed commands in order
and origin holds the line once (the archive holds the stranded commit). The
origin-unmoved case now expects the publish act alone. Red at ff12b01: 3
failed; green: 12 passed with the NOTED, signal and partial neighbours.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 3373d5be2b3599e6cec93a8d1a3d054ee1e98c8a)
…o its five exact names and to its `-L` guard, and a drift guard reads the names `lanes-edit.sh` writes there from its own text (lane openRepoTools-3's SHOULD 391, #198, slice 4; tests only)

Two mutants of MUST 351's hold passed the whole module: a prefix match
(`/^removed/` would remove a person's `removed.bak`), and the guard's
`|| [ -L ... ]` dropped (a dangling `.git/lanes-edit` link then holds
nothing). Lane openRepoTools-3's 19 proposed cases, copied into the
module in its style:

- `test_a_name_that_only_looks_like_scratch_holds`: 12 look-alike names
  (a backup, a suffix, another case, a hidden file, a space, a leading
  dash) each hold, in the dry run and under `--yes`, and the file stays;
- a subdirectory holds; a dangling link holds; an empty folder holds
  nothing, and the checkout is removed;
- the drift guard: one case per name `lanes-edit.sh` writes into the
  folder (`$cs_tmp/<name>`, `.../lanes-edit/<name>`), read from the
  script of the tree under test, each removable alone, so a sixth
  scratch name fails until `lane-end` knows it. One more case asserts
  the guard finds `fetch.err`, so a renamed variable cannot leave it
  with no case.

Both mutants are KILLED by these cases.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 6b3c308cb702a7b53ef98afa1717fb2c39f8ddc2)
Brett Heap and others added 26 commits October 22, 2026 21:43
… are pinned, the move asked again whether its path is directly under `<W>-lanes/`, the lock let go when the move fails, and a run whose only removal did not finish recorded (lane openRepoTools-3's SHOULD 374, #198, slice 4; tests only)

Three one-line mutants of the act passed the whole module. A case for
each:

- `test_a_lanes_folder_swapped_for_a_link_under_the_lock_holds`: a
  `git` first on PATH, after the candidate's last hold read under its
  lock, copies `<W>-lanes/` (its lock and pid line included) to a decoy
  and puts a link to the decoy in its place. Only `sc_under_home`,
  asked again at the move, stops it: the checkout is held, and the
  decoy's copy stays. So its false branch is reachable.
- `test_a_checkout_that_cannot_be_moved_aside_lets_its_lock_go`: an
  `mv` first on PATH refuses the move to the aside; the checkout is
  held, its lock is gone, and the next candidate is removed (its own
  lock would otherwise leave the first one's for good).
- `test_a_removal_that_does_not_finish_exits_1` is parametrized: alone,
  with no removal that finished, the PARTLY run still writes its NOTED
  line, naming the aside.

Each of the three mutants is KILLED by its case.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 75c711859fb8cbfe571ed96c678096211333cbb3)
…mes no transcript uuid, so a register or a log that could not be read refuses `--yes` as a read that failed, with its reason (lane openRepoTools-3's finding 545, #198, slice 4)

`--yes` with no `LANES_SESSION` reads the runner's uuid with
`lanes-edit.sh last-session <runner>`, and took ANY nonzero exit as "names
none" (`|| sc_uuid=""`), refused with 2. `last-session` exits 8 only where
no uuid is knowable, and 1 where the register or the log could not be
read; lane openRepoTools-4's #303 makes the 1 come for an unreadable log
too. A read that failed is not an answer.

Now 0 is the uuid, 8 is "names none" (2, as before), and any other exit
refuses with 1 ("Nothing was removed"), naming the exit and the writer's
last line of stderr, which is captured for that (a capture that cannot
be made names the exit alone).

`test_a_last_session_that_cannot_be_read_is_never_none`: a `last-session`
stub that exits 1 refuses with 1 and its words (red before: 2, "names
none"), and one that exits 8 still refuses with 2.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 24e3c6d6bb15c51d997df855d1fdd2f5726fcc3b)
…ere clones are made, under W's parent to depth 6, hidden ones too, and under `$TMPDIR` and `$HOME` to depth 4, and a scan that cannot finish holds every candidate (lane openRepoTools-3's SHOULD 352, with NITs 378 and 356, #198, slice 4)

`sc_dep_scan` read alternates files in three places only: W, the entries
directly in W's parent, and `<W>-lanes/`. A `--reference` clone of a
CLOSED lane's checkout in `.lane-worktrees/`, in a project's `review/`, or
in `$TMPDIR` held nothing, and `--yes` removed what it borrows from.

Now `find` lists every object directory (pruned there) and every `.git`
file under W's parent to depth 6 and under `$TMPDIR` and `$HOME` to depth
4, by their real paths; the lines are deduplicated, since the roots can
nest. The dry run names the scope, and the scan is read again under each
lock, as before. A repository at or under a candidate, or under a path the
run removes, goes with it.

- A scope that cannot be entered, a `find` that fails for anything but a
  directory it cannot read, or an alternates or `.git` file that cannot
  be read (NIT 356; a mode-000 `.git` file was skipped) holds every
  candidate: "what leans on it cannot be told: ...".
- A directory `find` cannot read (another user's, or gone while read) is
  counted once and said, and holds nothing, by question 2's rule.
- The lines are written TARGET FIRST (NIT 378), so a TAB in the
  dependent's own path no longer hides it.

Tests: `test_a_dependent_anywhere_in_the_scope_holds` (5 places),
`test_the_dry_run_names_the_dependents_scope`,
`test_a_dependents_scan_that_fails_holds_every_candidate`,
`test_a_directory_the_dependents_scan_cannot_read_is_counted_and_holds_nothing`,
`test_a_dependents_file_that_cannot_be_read_holds_every_candidate` (2).
Red before: 10 failed. Green: 10, plus 17 neighbours (leaves first, the
held-parent chain, the store inside a clone, the `.git` file cases).

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e04d26396dd20e9abbd60c7ca8dadfa289d40d02)
…o, under every hold a lane's checkout has and one more, a live process whose `LANES_REPO` names the clone (Brett Heap's ruling on #308's question 1, D37, with lane openRepoTools-3's conditions, #198, slice 4)

Brett Heap ruled on #308's question 1 (2026-10-22T12:46:57Z, relayed by
lane openRepoTools-3): "yes, have the sweep delete the 139 clones". The
per-profile clones beside W, which this build only listed, are now
candidates, in one leaves-first order with the checkouts under
`<W>-lanes/`, under lane 3's conditions:

- 352's wider dependents scan comes first (e04d263), since 47 of the
  139 borrow objects through alternates.
- Every hold applies: (a)-(c), `.git/lanes-edit/` (351) and the mounts
  (371). Unpublished content stays until its owner acts.
- A NEW HOLD: a live process whose `LANES_REPO` (read from
  `/proc/<pid>/environ` by the shell itself, NUL by NUL) names the clone,
  or a path in it, as spelled or by its real path. A process whose
  environment cannot be read is counted and said, and holds nothing. With
  no `/proc`, a clone beside W is held as "cannot tell". `LANE_END_PROC`
  is the test seam.
- The clone the caller's `LANES_REPO` names is kept.

A clone beside W is no lane's, so no lane state is asked of it: under
its lock, after its fetch, whether it is still a register clone is asked
again. It is moved aside beside W, never into `<W>-lanes/`, and an aside
left there is named by every later run. `sc_under_home` now takes a path
directly in W's parent too, never W or `<W>-lanes/`.

The NOTED line names the clones beside W it removed apart from the lane
checkouts, each character outside a lane name's written as `?`, and
counts those left. The dry run and the act's summaries count both places.

The module's box keeps its three clones beside W held by default (an
untracked note in the leaf), so every other case is as it was. 13 new
cases (leaves first with a name holding a space, a held middle clone,
four holds, a mount and a working directory, `LANES_REPO` by path and
through a link, no `/proc`, the caller's own `LANES_REPO`, an unfinished
removal beside W) and 4 changed ones: red at e04d263, 17 failed; green,
17 passed. With them, the 87 cases of the act, the holds, the dry runs,
the locks, the dependents and the NOTED line pass, and hygiene's 253.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit dba02941780592ff253f28151292a1074c57cd1a)
…wrapped, and a case holds every comment line of the sweep's section to 80 (lane openRepoTools-3's NIT 394, #198, slice 4)

The holds' comment line that ends "No `origin/<branch>` to compare"
stood at 93 columns, in a block that is otherwise 80 or less. It is
wrapped, and the rest of its paragraph with it; no word changes.

`test_the_sweeps_comments_are_80_columns_or_less` reads `lane-end` from
the section's header, `# ---- AMENDMENT 23 S6.2`, to the `fi` that ends
it, and fails on any comment line over 80 characters (characters, not
bytes: a dash is one column). Red before the wrap (line 1381, 93), green
after; its mutant, one comment line lengthened again, is KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 433f759beb91dbdf44ece97c410fc64183e5d8bc)
…listed, or whose real path cannot be read, refuses the run with 1 instead of reading as "nothing to remove" (lane openRepoTools-3's NIT 377, #198, slice 4)

With `<W>-lanes/` at mode 000, `sc_real` could not enter it, so its
real path read as empty, and the sweep said it "resolves through a link
to , which this sweep never follows" and exited 0, in the dry run and
under `--yes`: a read that failed, reported as nothing there. It is now
asked first whether it can be entered and listed, and whether its real
path can be read; when it cannot, the run refuses with 1 (D10: a read
that failed) and "Nothing was removed", before anything beside W is
looked at either.

`test_a_lanes_folder_that_cannot_be_entered_or_listed_is_refused`
(skipped as root): the dry run and `--yes` both exit 1 with that reason,
and every checkout and clone stays. Red before the fix (the dry run
read on and exited 3, the clones beside W being free); green, with the
dry runs and the lane-checkout W case, and hygiene. Its mutant, the new
test dropped, is KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 9b19e124e7b183024bf11ddc081c936f78350612)
…` that is W refuses with 2 before anything is removed, in S5's words (lane openRepoTools-3's NIT 376, #198, slice 4)

Under `LANES_ROUTE_WRITES=1`, `lanes-edit.sh` refuses any write through
a `LANES_REPO` that is W, the readers' copy (Amendment 23 S5), by its
real toplevel or its git common directory. The sweep removed a checkout
first, then its NOTED line was refused, and the by-hand line it printed
was refused again in that window. The preflight now asks the same
question of the caller's `LANES_REPO` before the first removal and
refuses with 2, naming the lane checkout the line would go through.
Without routing, `LANES_REPO=W` is the default write path, and is left
as it is; the dry run writes nothing and is not refused.

`test_yes_with_LANES_REPO_naming_W_is_refused_before_anything_is_removed`,
W named by its own path and by a linked worktree of it (one git common
directory): `--yes` exits 2 with S5's words, the CLOSED lane's checkout
stays, and nothing reaches origin; the dry run still exits 3. Red before
the fix (the act ran, exit 1); green after, with the other `LANES_REPO`
cases, the preflight refusals and hygiene. Its mutant, the new test
dropped, is KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 0f564e84ad17149df57854e7330eaae3f4dbb49b)
…is cleaned as `lanes-edit.sh`'s `lock_ident` cleans its own, `:` kept and a bad container `unknown` (lane openRepoTools-3's NIT 359, #198, slice 4)

A writer of a checkout compares the pid line of the lock it meets with
its own `lock_ident` to know a holder in its own container, whose pid it
can ask; any other line is aged by the directory's mtime alone. The
sweep kept only `[A-Za-z0-9._-]` and mapped anything else, and an empty
value, to `none` (container) or `unknown` (host), while `lock_ident`
keeps `:` too (`lanes_binding_value_ok`), and maps an empty container to
`none` and a bad one to `unknown`. So a container named with a `:` made
the sweep's line read as another container's, and a killed sweep's lock
was waited out for ten minutes instead of taken back at once. The
sweep's line now follows `lock_ident` character for character.

`test_the_locks_pid_line_is_lanes_edits_own`: a hook at the CLOSED
lane's fetch copies the pid line the sweep wrote; with
`LANES_CONTAINER=bench:a` it ends ` bench:a hosta`, with `bench a` it
ends ` unknown hosta`, and with `LANES_HOST=host:1` it ends
` bench-a host:1`. Red before the fix (`none`, `none`, `unknown`);
green after. Its mutant, `:` dropped from the container's set, is
KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 909dce4c46f481ed5220a21866eb611a8e707d41)
…cters of the checkout's, so a long name is moved aside instead of held in every act (lane openRepoTools-3's NIT 379, #198, slice 4)

The aside was `.<name>.removing-<UTC>-<pid>`, which grows with the
name: a 240-character name, with its 35-character suffix, is past
NAME_MAX, so its `mv` always failed, and the checkout was `remove` in
every dry run and held "could not be moved aside" in every `--yes`.
The aside is now `.<the name's first 48 characters>.removing-<UTC>-<pid>`,
at most 84 characters, still named for a person and still matched by
`.?*.removing-*`, so every later run names one that is left.

`test_a_long_name_is_moved_aside_under_a_name_that_does_not_grow`: a
240-character clone beside W is `remove` in the dry run and `removed`
under `--yes`, through an aside (seen by an `mv` first on PATH) whose
name is the first 48 characters and the suffix, and no aside is left.
Red before the fix (held, could not be moved aside); green after.
`l3-d37-aside` is re-pointed at the new line, and the new mutant, the
whole name kept, is KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 1bd64905f14f7859a5e43272d0a98f0a0b91346a)
…son the report prints is written as `?`, so a name with a newline writes no line of its own (lane openRepoTools-3's NIT 358, #198, slice 4)

The report printed names raw: a newline in a name under `<W>-lanes/`
wrote a line of its own into it, one that could read as another
entry's (`remove FORGED ...`), and a TAB in a dependent's path was
printed inside a hold's reason. The decision was always right; only the
report could be forged. Every entry line (`keep`, `hold`, `remove`,
`removed`, `PARTLY`, `leftover`) now writes each control character of
its path and its reason as `?`, by the shell's own `${x//[[:cntrl:]]/?}`
(no process per name). The commands the `leftover` line hands out keep
`%q`, which a shell reads back exactly.

`test_a_name_with_a_control_character_writes_no_line_of_its_own`: a
directory under `<W>-lanes/` named `odd`, a newline, and a line that
reads as the removal of `FORGED`: in the dry run and under `--yes`
there is one line naming it, a `keep` whose path ends `odd?    remove
...`, and no `FORGED` entry. Red before the fix; green after. The TAB
dependent case of 352 now expects `tab?here`. Its mutant, the `keep`
line's mapping dropped, is KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit ed8576fc3c8b0dc28c2fb8faf0e4a3f4684fe542)
…are named beside it, in the dry run and the act (lane openRepoTools-3's NIT 354, #198, slice 4)

A commit taken off its branch (`reset --keep HEAD~1`) is on no ref, so
nothing held the checkout, and the removal took the reflog that could
undo a mistaken reset, unsaid. Such commits are now named, as ignored
files are: "commit(s) only its reflog reaches, which go with it: N
(<short shas>)", from `git rev-list --abbrev-commit --reflog --not
--all`, on the `remove` line of the dry run and the `removed` line of
the act. They do not hold: a write replayed after a lost push race
leaves one whose patch origin has, so a hold would keep such a checkout
for good. Lane openRepoTools-3 offered "hold, or name"; this names.

`test_commits_only_the_reflog_reaches_are_named`: a commit in the
CLOSED lane's checkout, then `reset --keep HEAD~1`; the dry run's
`remove` and the act's `removed` name it by its short sha. Red before
the fix; green after. Its mutant, the read dropped, is KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 307b9ca518591cfa477be7deddbf093a9450f0f0)
…nored editor's swap file hold the checkout, annotated tags are named, and the comment that called ignored files the register's own rules is put right (lane openRepoTools-3's NIT 355, #198, slice 4)

Ignored files hold nothing, and are named. But `--exclude-standard`
reads the checkout's `.gitignore` files, its `.git/info/exclude` and the
user's `core.excludesFile`, not the register's rules alone, as the
holds' comment said. Two things such rules can hide are not disposable:

- a repository of its own at an ignored path, which may hold commits
  nothing else has (at any other path it holds, as untracked): lane 3's
  probe, one in `.git/info/exclude`'s `vendor/` with a commit of its
  own, gave `remove`;
- an editor's swap file (`*.swp`), which says a person has a file of
  the checkout open.

Each ignored directory is looked in (`find`, `.git` pruned) and each of
these holds; an ignored directory that cannot be looked in holds too.
An annotated tag on a published commit reaches nothing origin lacks and
holds nothing, but its tag object goes with the checkout, so it is
named beside it, as ignored files are.

`test_what_ignored_rules_hide_that_may_be_work_holds` (a repository at
an ignored path, an ignored swap file): each holds, in the dry run and
under `--yes`, and the checkout stays as it was.
`test_annotated_tags_are_named`: an annotated and a lightweight tag on
the published HEAD; the dry run's `remove` and the act's `removed` name
the annotated one alone. Red before the fix (3 failed); green after.
Its mutants, the repository hold and the swap hold dropped, are KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 9e0cbd1588f36459659688f071df3f6cc7ee4942)
…` with 2, and a candidate gone at its turn is reported as gone, not held (lane openRepoTools-3's NIT 375, #198, slice 4)

Two `--yes` runs at once removed nothing twice, but each one's NOTED
line said the other's removals were "held and left", and one candidate
was left by both. `--yes` now takes a lock of its own before it reads or
removes anything: `<W's git directory>/lanes-sweep-checkouts.lock`,
made by `mkdir`, with `lanes-edit.sh`'s pid line, and let go on every
way out by the exit's `cleanup` unless its line names another holder. A
second `--yes` is refused with 2, naming the lock and its pid line, and
how to remove one a killed sweep left. The lock is in W's git directory,
not `<W>-lanes/` as lane 3 sketched, because since D37 the clones beside
W are swept when there is no `<W>-lanes/` at all.

A candidate that is no longer there at its turn (a person, or another
workstation's sweep, removed or moved it) is reported `gone`, not held
for want of a `lanes/` directory, and is neither named removed nor
held and left in the NOTED line.

`test_a_second_sweep_is_refused_while_one_runs`: a hook at the CLOSED
lane's fetch, inside the first sweep's act, runs a second `--yes`; it
exits 2 with the lock named and prints nothing, and the first removes
the checkout, writes one NOTED line and lets its lock go.
`test_a_candidate_gone_at_its_turn_is_said_as_gone`: a hook at the
first candidate's fetch moves the second away; it is `gone`. Red before
the fix; green after. Its mutants, the lock taken by `mkdir -p` and the
gone test dropped, are KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 51ee5ff243fe36261fd5560c94d6dc3918084303)
…utex_steal`'s rename is: the lock's pid line, the working directories and what leans on the checkout are asked again of what was moved, and any of them moves it back (lane openRepoTools-3's NIT 353, #198, slice 4)

Under the lock, the working directories and the dependents were read
before about ten git reads, and the pid line just before the `mv`, so
three things could come between a read and the move unseen: a process
that came to work in the checkout during the git reads, a `--reference`
clone of it made then, and a writer that took the lock over between the
pid-line read and the move. Lane 3's probes removed the checkout in all
three. The window is short, but a removal is the one act nothing
undoes.

After the move, before the removal, the sweep now asks of what was
moved: the pid line of the lock that went with it, read again; the
working directories, read again against the aside's path (a process's
working directory follows the rename); and what leans on it, read again,
with an alternates or `.git` file whose target is gone kept as written,
so a clone that names the old path is matched. Any of them moves the
checkout back to its path and holds it, saying so; its lock is let go
unless another writer holds it now. The lock is refreshed just before
the pid-line read.

Three cases, red before the fix (each `removed`), green after: a `git`
first on PATH, right after the last read under the lock, starts a
process in the checkout, or makes a `--reference` clone of it; an `mv`
first on PATH writes another holder's pid line into the lock just
before the move. Each is held "... as it was moved aside ...; it was
moved back", and no aside is left. Their mutants are KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 811c10fc0fc57affb27a1dfa7fed982ff2d82622)
…lds the clone it names, the case a kept CR fails, since NIT 353 keeps a target that does not resolve as written (the one survivor of the battery at 811c10f, #198, slice 4; tests only)

The mutant `c3-3-crlf` (`tr -d '\r'` replaced by `cat` in the
dependents scan's `.git` file read) was KILLED at e04d263 and at every
commit after it up to 51ee5ff, and SURVIVED at 811c10f (NIT 353). Its
one case wrote `gitdir: <clone>/.git` and a CRLF. Since 353 a target
that does not resolve is kept as written, so that the clone a moved
checkout leaves behind is still matched; with the CR kept,
`<clone>/.git` and the CR after it is still under `<clone>/`, and the
clone was held anyway.

The CR handling is not redundant: a relative path, as git writes a
submodule's `.git` file, resolves only once the CR is gone. With the CR
kept, `<dir>/../<W>-lanes/<lane>/.git` and its CR does not resolve, is
kept as written, is under no candidate, and the clone is removed.
`test_a_git_file_naming_a_clones_git_directory_holds_it` takes a fourth
parameter, `CRLF and relative, whose path resolves once the CR is
gone`; git itself reads that file as the clone's git directory, as the
case asserts first. The mutant is re-pointed at it, and is KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 80bd3922ed3cd01b36da8004f0befa9693f55868)
…NES_WORKSPACE_ROOT` names another directory than workspace.yaml's `path:`, and that directory, or a clone beside W with a `<name>-lanes/` of its own, is never a candidate (rv4b's MUST A on 811c10f, D45, #198, slice 4)

rv4b's pre-review of 811c10f found that `LANES_WORKSPACE_ROOT` set to a
per-profile clone beside the real W made that clone "W", and the real
W, the readers' copy that workspace.yaml's `path:` names, "a clone of
the register beside W": with nothing borrowing from it, `--yes`
removed it, and the second-`--yes` lock moved into the substitute's
git directory.

- `path:` is read from workspace.yaml as every helper reads it, by
  `lanes_ws_field`, which no `LANES_WORKSPACE_ROOT` reaches. Where the
  variable is set and its real path is not `path:`'s, `--yes` exits 2
  before the lock and before any listing, naming both and saying to
  unset the variable or point it at `path:`. Nothing is removed.
- The dry run still runs, and its header says that `--yes` refuses.
- `sc_is_a_w` keeps the directory `path:` names, and a clone beside W
  with a `<name>-lanes/` beside it (a W with lane checkouts of its
  own), at the listing and again under the clone's lock before the act.
- The seam stays (D45): the shared resolver is byte-identical in the
  four lane helpers, and the module and the shell suite point helpers
  at a sandbox through it. A value whose real path is `path:`'s, as
  spelled or through a link, is no refusal. The header's text says
  lane windows export it too, and names the refusal.

Seven new cases in `tests/test_a23_sweep_checkouts.py`: 5 failed and 2
passed at 80bd392; the module's MUST A and neighbour selection with
`tests/test_repo_hygiene.py`, 291 passed. Six new mutants
(`rv4b-A-refuse`, `-real`, `-path`, `-lanes`, `-relock`, `-dry`), all
KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit c73a9afc6ebfa9569a3d44947fa5be89ffddee65)
…side, the windows' `LANES_REPO` and the mount table are read again, and a window that came to name the old path, a mount that came to be in the aside, or no table or `/proc` to tell by, moves it back (rv4b's SHOULD D on 811c10f, #198, slice 4)

rv4b's pre-review of 811c10f found that NIT 353's re-check after the
move asked the lock's pid line, the working directories and what leans
on the checkout again, but not D37's `LANES_REPO` hold or the mounts:
both were read before the git reads under the lock, so a window opened
on the CLOSED lane's checkout during them (probe P4) was missed, and the
checkout removed under it.

- After the move, `sc_repo_scan` runs again, and a live process whose
  `LANES_REPO` names the old path, as spelled, holds it; with no `/proc`
  then, a clone beside W is held as "cannot be told", as D37 holds it at
  the listing and under the lock.
- `sc_mount_scan` runs again, and a mount point at or under the aside
  (the move carried it) holds it; no mount table that names `/` holds it
  as "cannot be told".
- Each moves the checkout back, its lock let go, as the other three do.

Four new cases in `tests/test_a23_sweep_checkouts.py` (a window opened
after the last read, a mount made as it is moved aside, no table then,
`/proc` gone then beside W): 4 failed at c73a9af (`removed` for `hold`).
With the fix, the module's post-move, mount, window and `/proc`
selection, 27 passed; `tests/test_repo_hygiene.py`, 253 passed. Seven
new mutants (`rv4b-D-repo`, `-repo-scan`, `-reponone`, `-mount`,
`-mount-scan`, `-mountnone`, `-aside`), all KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 2bc9b667617dd51cb942fa3a1b11ad6a334bedab)
… scan cannot read holds every `--sweep-checkouts` candidate, in words that name it and `chmod`; another user's is still counted and holds nothing (rv4b's SHOULD B on 811c10f, #198, slice 4)

rv4b's pre-review of 811c10f found that every directory the scan could
not read was counted and held nothing, this user's own too: a
`--reference` clone with a commit of its own, kept in a mode-000 folder
beside W (probe P2), was not seen, `--yes` removed the CLOSED lane's
checkout, and the clone's `fsck` then exited 10.

- Each "Permission denied" line of `find` names the directory: GNU
  `find` quotes it in the C locale (`'P'`, a quote as `\'`, a backslash,
  TAB, newline and non-ASCII bytes as escapes), BSD `find` does not;
  `sc_find_err_path` undoes the quoting.
- Where that directory is this user's own (`[ -O ]`), the scan cannot
  finish (`sc_dep_fail`), which holds every candidate: the words name
  the directory and `chmod u+rx -- <it>`.
- Another user's directory, or one gone while read, is counted and said,
  and holds nothing, as decided under Q2.

The module's case for a mode-000 directory beside W is changed to match:
it now holds both CLOSED lanes' checkouts, with a dependent inside it, a
plain name and one with a quote, a blank and a backslash. A new case
keeps another user's directory counted (a `find` first on PATH says it
could not read one, since a test cannot make one without root).

Red at 2bc9b66: 2 failed, 1 passed (the own-directory cases, rc 3
for 0). Green: the module's dependents, scope and unreadable-file
selection 26 passed, tests/test_repo_hygiene.py 253 passed; the five
new mutants (rv4b-B-own, -any, -quotes, -unquote, -escapes) KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e12d077630b5017ef7cea9b69bb020e669cabd2b)
…epth 8, where a Claude Code scratchpad keeps its clones, and the scope line names each root's depth (rv4b's SHOULD C on 811c10f, #198, slice 4)

rv4b's pre-review of 811c10f found that `$TMPDIR` was scanned to depth
4 only. A Claude Code scratchpad is
`/tmp/claude-<uid>/<project>/<session>/scratchpad`, so a clone made in
one stands at depth 5 and its object directory at 7 (probe P3): it was
not seen, `--yes` removed the checkout it borrowed from, and its `fsck`
exited 10.

- `sc_dep_root 8 "${TMPDIR:-/tmp}"`: repositories to depth 8, so a
  clone in a folder of a scratchpad's own (three levels further) is
  seen too.
- The dry run's scope line takes each root's depth from the scan's own
  list, and says why `$TMPDIR` goes deeper.

`DEPENDENTS_AFAR` takes a clone eight levels under `$TMPDIR` in a
scratchpad's shape, and one in `$HOME` (rv4b's unpinned `:1675`).

Red at e12d077: 2 failed, 6 passed (the scratchpad clone and the scope
line). Green: the module's dependents and scope selection 18 passed,
tests/test_repo_hygiene.py 253 passed (the comment names the
scratchpad's shape relative to `$TMPDIR`, not a host path); the
mutants rv4b-C-depth, rv4b-C-scope, r4b-home-depth (rv4b's) and
l3-352-tmpdir (re-pointed at depth 8) KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 7d194593726d9ccb3e645cc132f10f7209bce383)
…e is spelled from the real path of the deepest directory on it that does, and the read after the move matches the `.git` files too, so a `.git` file or a relative `../` path written after the last read still names the moved checkout, and the checkout is moved back (the 353 gap, D44's note, D46, opensoft/openRepoTools#198, slice 4)

NIT 353 kept a target that does not resolve as written, so that a
clone made to lean on a checkout's old path, after the move, is still
matched. A relative target with `..`, as git writes a submodule's `.git`
file, or a relative alternates line, was kept with its `..`, named no
candidate, and the checkout was removed under it (D44's note, read from
the code then; now a case).

- `sc_real_or_text`: the real path where it resolves; otherwise the real
  path of the deepest directory on it that stands, the rest appended as
  written. An absolute target reads as before.
- The read after the move (NIT 353's commit point) matched only the
  alternates lines: the `.git` files were read under the lock alone, so
  a `.git` file written between the last read and the move, absolute or
  relative, was never matched, and the checkout was removed under the
  directory that works in it (the one case still red on 10-24). It now
  matches the `.git` files too, by the alternates' rule (what went with
  the aside is not matched), and says "its git directory is named by
  the .git file of <dir>, which works in it and came as it was moved
  aside". New case: an absolute `.git` file written after the last read.
- A CRLF line whose CR is kept now reads under the clone too, whether
  absolute or relative, so `c3-3-crlf`'s kill moves to a new
  `GIT_FILES` case: a CRLF line whose last name is a link to the clone's
  git directory, which resolves only once the CR is gone (D46).

Red, before this commit: with `sc_real_or_text` alone (10-24), the
selection below ended 1 failed, 31 passed:
`test_a_relative_path_written_after_the_last_read_moves_it_back[.git file]`,
`removed` for `hold`. With this commit, the module's 353 selection
(`relative_path_written or git_file or after_the_last_read or
moves_it_back or dependent or comments_are_80`): 33 passed.
`tests/test_repo_hygiene.py`: 234 passed, 18 skipped, 1 failed,
`test_the_root_carries_the_line_ending_rule`, which reads a
`.gitattributes` that the code leg's main has and this slice's base does
not yet (slice 1b' brings it). Seven mutants, all KILLED: `rv4b-353-text`,
`rv4b-353-gitfile`, `rv4b-353-alt`, `r4b-rel-alt`, `c3-3-crlf`
(re-pointed), and two new: `w4r-353-post-gitfile` (the read after the
move without the `.git` files, as before this commit: the new case gives
`removed` for `hold`) and `w4r-353-post-lines` (that read given the
alternates lines again).

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ck, it asks where the checkout went, so a directory that came to stand at its path meanwhile, and took it inside, is said with the checkout's place, not as "moved back" (rv4b's NIT G on 811c10f, opensoft/openRepoTools#198, slice 4)

rv4b's pre-review of 811c10f: between the test that the old path is
free and the `mv` that moves the aside back, a directory made at the
old path takes the aside inside it, as `mv` moves into a directory, and
the row said "it was moved back" over a checkout hidden in that one.
BSD `mv` has no `-T`, so the move is not changed; where the aside went
is asked after it:

- `<old path>/<the aside's name>` stands: the row says the checkout
  could not be moved back, that a directory came to stand at its path
  as it was, and where the checkout is now, which no later run names
  (move it out by hand); its lock is not let go.
- Otherwise "it was moved back", and the lock let go, as before.

`test_a_directory_made_at_its_path_as_it_is_moved_back_is_said`: an
`mv` first on PATH writes another holder's pid line into the lock just
before the move aside, so the checkout is held and moved back, and
makes a directory at the old path just before the move back.

Red, the new case alone before the fix: 1 failed (the row said "it was
moved back" over the checkout inside the new directory). With the fix,
the module's selection `directory_made_at_its_path or moves_it_back or
moved_back or comments_are_80`: 12 passed. `tests/test_repo_hygiene.py`:
234 passed, 18 skipped, 1 failed, `test_the_root_carries_the_line_ending_rule`
(the `.gitattributes` slice 1b' brings; as at the parent). The new mutant
`rv4b-G-nested` (the check after the move back off): KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is split in two, so a failure names the half that failed (rv4b's NIT H on 811c10f, Sonar S9073, opensoft/openRepoTools#198, slice 4)

rv4b's pre-review of 811c10f named four asserts that join two checks
with `and` (Sonar S9073); a fifth of the same shape is split with them:

- `test_a_former_name_of_the_runners_own_lane_is_kept_for_its_own_reason`:
  `old.is_dir()`, then `new.is_dir()`;
- `test_a_lanes_folder_swapped_for_a_link_under_the_lock_holds`: the
  shim's marker ("the shim ran"), then `<W>-lanes/` a link;
- `test_a_dependents_scan_that_fails_holds_every_candidate`: each
  checkout a directory;
- `test_clones_beside_W_are_removed_leaves_first`: W, then `<W>-lanes/`;
- `test_a_held_clone_beside_W_keeps_what_it_borrows_from`: each clone.

Text only: no case changes what it checks, and lane-end is unchanged.

The five cases' selection: 6 passed (one case has two parameters).
`tests/test_repo_hygiene.py`: 234 passed, 18 skipped, 1 failed,
`test_the_root_carries_the_line_ending_rule` (the `.gitattributes` slice
1b' brings; as at the parent). No mutant: no behaviour changes.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efusal of a lane say that the sweep looks at the register clones beside W too, as it has since D37 (rv4b's NIT I on 811c10f, opensoft/openRepoTools#198, slice 4)

rv4b's pre-review of 811c10f found text that D37 (Q1, answered by
Brett Heap: the per-profile clones beside W are swept too) left stale:

- the usage line named only the checkouts under `<W>-lanes/` that
  CLOSED lanes left; it names the register clones beside W too;
- exit 3 said "something under `<W>-lanes/` is removable"; it says "or
  a register clone beside W";
- the refusal of a lane said the sweep "looks at every register
  checkout under <W>-lanes/"; it adds "and every register clone beside
  W".

The header's "test seam" text was the third item; MUST A (D45) already
rewrote it.

`test_the_sweep_takes_no_lane_and_no_other_act[a lane]` now reads the
refusal's words.

Red, the case before the fix: 1 failed (the refusal named
`<W>-lanes/` only). With the fix, the module's selection
`takes_no_lane_and_no_other_act or dry_run_with_nothing_to_remove or
comments_are_80`: 10 passed. `tests/test_repo_hygiene.py`: 234 passed,
18 skipped, 1 failed, `test_the_root_carries_the_line_ending_rule` (the
`.gitattributes` slice 1b' brings; as at the parent). The new mutant
`rv4b-I-refusal` (the refusal's old words): KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hose processes were read, and says that another container's processes, their working directories and their windows' `LANES_REPO` are not seen (rv4b's NIT J on 811c10f, from Q2's answer, opensoft/openRepoTools#198, slice 4)

rv4b's pre-review of 811c10f: Q2's answer asked the header to say whose
processes the sweep reads. `/proc` (or `lsof`) shows the processes of
the container it runs in, so beside W an old window in another
container on the same host, writing through a per-profile clone, is not
seen, and the header did not say so.

- The header's new `processes` line: `those container <c> on host <h>
  shows; another container's processes, their working directories and
  their windows' LANES_REPO, are not seen`.
- The container and host are read from `lanes-edit.sh workstation` in
  the dry run too (before, only under `--yes`, for the lock's pid
  line), cleaned as before.
- `/proc/<pid>/fd` is not read: by decision this round (D48), not code.

`test_the_dry_run_names_the_dependents_scope` reads the line, with the
box's `bench-a` and `hosta`.

Red, the case before the fix: 1 failed (no `processes` line). With the
fix, the module's selection `dependents_scope or pid_line or dry_run or
header or workstation or comments_are_80`: 10 passed.
`tests/test_repo_hygiene.py`: 234 passed, 18 skipped, 1 failed,
`test_the_root_carries_the_line_ending_rule` (the `.gitattributes` slice
1b' brings; as at the parent). Two new mutants, `rv4b-J-line` (no
`processes` line) and `rv4b-J-dry` (the dry run does not read the
workstation), and `l3-359-colon` re-pointed at the block, now two
columns left: all three KILLED.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…code leg's main 577110e) into slice 4 with --no-ff: git found no conflict, since the one file both change, lane-end, changes in places apart (opensoft/openRepoTools#198, slice 4)

Slice 1b's code-leg branch, feat/198-a23-slice-1b-guard-publish at
34799b7, as the coordinator named it.
It brings slice 1a' and, through it, the code leg's main 577110e: the
front door (with the root's .gitattributes), T017's five lanes-edit.sh
fixes, and the bash 3.2 `ps` subshell in lane-end's dh_settle_gone
(opensoft/openRepoTools#292), outside the sweep's section. The merge's
tree is the one `git merge-tree` gave before the merge was made.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es-edit.sh keeps in `.git/lanes-edit/claim-reads-<UTC>-<pid>.txt` for a claim's re-run, are the writer's scratch and hold no checkout; the drift guard found the name at the merge of slice 1b (D49, opensoft/openRepoTools#198, slice 4)

The merge of slice 1b's code-leg branch (34799b7) brought 8a88f50 (lane
openRepoTools-3's 706 on opensoft/openRepoTools#307): where a claim's
Rule 1 reads are over the cap that crosses its re-run's `exec`, attempt
1 keeps them whole in `.git/lanes-edit/claim-reads-<UTC>-<pid>.txt`,
and `cleanup` removes the file when the claim's last run ends; only a
killed claim leaves one. The sweep holds a checkout for anything in
`.git/lanes-edit/` but the writer's own scratch, so a leftover one held
its checkout for good. SHOULD 391's drift guard, which reads its cases
from lanes-edit.sh's text, failed on it at the merge.

- `.git/lanes-edit/`'s filter: a plain file whose name starts
  `claim-reads-` is scratch too, by find's own test, as the five exact
  names are. It is a copy of what GitHub keeps. A prefix, since the name
  carries a UTC and a pid; a folder of that name, or a name with
  anything before the prefix, still holds (NIT 392's rule).
- New case `test_a_claims_whole_reads_alone_hold_nothing` (the name as
  lanes-edit.sh writes it, removed under `--yes`); `xclaim-reads-1.txt`
  joins the look-alikes that hold, and a folder named
  `claim-reads-1.txt` the kinds that are not scratch.

Red at the merge e402a23, the whole module: 1 failed, 194 passed
(`test_every_other_name_lanes_edit_writes_there_holds_nothing[claim-reads-]`,
`hold` for `remove`). With the fix, the module's selection `lanes_edit
or scratch or claims_whole_reads or comments_are_80`: 39 passed.
`tests/test_repo_hygiene.py`: 235 passed, 18 skipped. Two new mutants,
`w4r-D49-claim-reads` (the exemption taken away: the new case and the
guard's fail) and `w4r-D49-prefix` (`*claim-reads-*`: the look-alike
fails): both KILLED. The four mutants of the filter's find line
(`l3-351-scratch`, `l3-391-prefix`, `l3-392-kind`, `l3-392-error`) are
re-pointed at the new line for the battery.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

Mutants at 1f16bbc: the table

The table of the description's Mutants section, which says how each was run and what the last column means. 144 mutants: 144 KILLED, 0 SURVIVED, 0 ERROR.

Mutant One line changed First failing case The kill rests on
h-plus a + under git cherry does not hold test_holds[a + on main] behaviour
h-merge a merge commit origin lacks does not hold test_holds[a merge only] behaviour
h-ref another local ref reaching what origin lacks does not hold test_holds[a tag] behaviour
h-stash a stash does not hold test_holds[a stash] behaviour
h-staged staged or modified files do not hold test_holds[staged] behaviour
h-assume assume-unchanged entries do not hold test_holds[assume-unchanged] behaviour
h-untracked untracked files do not hold test_holds[untracked under handoffs] behaviour
h-op an operation in progress does not hold test_holds[a merge in progress] behaviour
h-lock its writers' lock does not hold the dry run test_holds[its writers' lock] behaviour
h-noorigin no origin/ does not hold test_no_origin_main_holds_the_dry_run behaviour
h-worktree a linked worktree does not hold test_holds[a linked worktree] behaviour
h-deps a dependent does not hold test_held_parent_keeps_its_dependents_chain[the leaf held] behaviour
h-cwd a live cwd does not hold test_a_live_cwd_holds reason only: 353's read after the move holds it, and moves it back (below)
a-fetch a failed fetch does not hold test_a_fetch_that_fails_holds behaviour
a-still the lane's state is not asked again under the lock test_a_lane_resumed_after_the_listing_is_kept behaviour
a-recheck the holds are not read again under the lock test_recheck_before_removal behaviour
n-live a LIVE lane is not kept by its state test_a_live_lanes_checkout_is_kept_by_the_live_rule[a LIVE lane] reason only: the class check keeps a LIVE lane too (below)
n-closed a lane that is not CLOSED is a candidate test_a_paused_lane_is_kept_without_LANES_REPO a PAUSED lane's checkout goes from keep to remove
n-runner the runner's own checkout is not kept as the runner's test_W_and_live_lane_never_candidates reason in the first case; run alone, test_a_closed_runner_keeps_its_own_checkout is killed by the checkout becoming a candidate
n-xrepo the caller's LANES_REPO is not kept test_W_and_live_lane_never_candidates reason in the first case; run alone, test_a_closed_lanes_checkout_named_by_LANES_REPO_is_kept is killed by the checkout becoming a candidate
n-link a symbolic link is not refused as one test_W_and_live_lane_never_candidates reason only: a second keep still holds it (below)
n-w W is not refused as W test_a_directory_sharing_Ws_git_directory_is_kept_as_W reason only: a second keep still holds it (below)
n-wlanes a W that is a lane checkout is not refused test_a_W_that_is_a_lane_checkout_is_refused exit code, 0 for 2: the sweep goes on beside a lane checkout
p-lane --yes without LANES_LANE is not refused test_yes_is_refused_before_anything_is_removed[no LANES_LANE] exit code and message only, 1 for 2: set -u stops it, nothing removed (below)
p-nofetch --yes under LANES_NO_FETCH=1 is not refused test_yes_is_refused_before_anything_is_removed[LANES_NO_FETCH=1] exit code and message only, 1 for 2: the listing's own refusal stops it, nothing removed (below)
p-uuid D19: a LANES_SESSION that is no uuid is not refused test_yes_is_refused_before_anything_is_removed[a LANES_SESSION that is no uuid] exit code, 1 for 2: the act ran and removed
o-leaves D21: candidates are taken in listed order, not leaves first test_leaves_first_in_one_run the order of the removals
o-under D18: only a clone's own .git/objects is leaned on test_a_store_kept_inside_a_clone_is_leaned_on behaviour
w-nofetch the NOTED line is written under LANES_NO_FETCH=1 test_noted_line_written_through_runner_checkout exit code, 1 for 0: the NOTED line is refused
w-rc a NOTED line not written is a quiet success test_noted_line_not_written_exits_1_with_the_command exit code, 0 for 1: an unwritten line is a quiet success
d-dry the dry run acts test_dry_run_lists_and_removes_nothing exit code, 0 for 3: the dry run took the act's path and removed a checkout
m1b-listing-off MUST 1: the listing's --yes refusal on a W fetch that did not complete is off test_a_W_fetch_that_fails_at_the_listing_refuses_yes[the fetch fails after ls-remote (fell-back)] exit code, 0 for 1: --yes goes on to the act on a register it could not fetch
m2-sc-fetch MUST 1: the re-ask ignores a W fetch that did not complete test_a_lane_resumed_while_W_cannot_fetch_is_kept behaviour
m3-sc-sessions MUST 1: the re-ask ignores unreadable session records test_session_records_unreadable_at_the_recheck_keep_the_checkout behaviour
m4-sc-register MUST 1: the re-ask ignores an unreadable register test_a_register_unreadable_at_the_recheck_keeps_the_checkout behaviour
m5-sc-capture rv4-r2 SHOULD 1: a capture not read back does not hold by itself test_a_capture_that_cannot_be_read_back_holds_by_itself behaviour
R1-no-cwd-rescan SHOULD 1 R1: the working directories are not read again under the lock test_a_cwd_that_appears_after_the_listing_holds reason only: 353's read after the move holds it, and moves it back (below)
R3-no-former-name-reask rv4 R3: the former-name re-ask never holds test_a_former_name_whose_new_checkout_goes_after_the_listing_is_kept behaviour
R4-no-partial-exit rv4 R4: no exit 1 for a removal that did not finish test_a_removal_that_does_not_finish_exits_1[beside a removal that finished] exit code, 0 for 1: a removal that did not finish
s2-exit3 SHOULD 2: log's exit 3 is reported as NOT written test_noted_line_unconfirmed_is_never_written_again wording: "was NOT written" for a line that landed, after the exit code held
s2-exit6 SHOULD 2: exit 6 gets the generic refusal (the arm dropped) test_noted_line_kept_unpublished_is_published_not_written_again wording: "was NOT written" for a line kept unpublished, after the exit code held
d29-listing D29: a former name beside its new checkout is a candidate whatever its lane (D27 and D29 undone) test_a_former_name_of_a_lane_not_closed_is_kept reason: an IDLE lane's former-name checkout goes from keep to hold, by D37's LANES_REPO hold (below)
d29-still D29: the re-ask passes a former name without asking its lane LIVE or CLOSED test_a_former_name_whose_lane_resumes_after_the_listing_is_kept behaviour
n1-gitfile NIT 1: a .git file naming a clone's git directory does not hold test_a_git_file_naming_a_clones_git_directory_holds_it[absolute] behaviour
n2-ign NIT 2: the act's removed line does not name ignored files test_ignored_files_are_named_at_the_act wording: the removed line's note of ignored files
n3-count NIT 3: a partial removal is counted as removed test_a_removal_that_does_not_finish_exits_1[beside a removal that finished] wording: the summary's counts
n4-clone NIT 4: what made it a register checkout is not asked again under the lock test_a_checkout_whose_origin_changes_after_the_listing_is_held behaviour
n4-owner NIT 4: a lock taken over before the move does not hold test_a_lock_taken_over_under_the_sweep_holds_and_is_left[then the move is asked] reason only: 353's read after the move holds it, and moves it back (below)
n4-drop NIT 4: sc_lock_drop removes a lock taken over test_a_lock_taken_over_under_the_sweep_holds_and_is_left[then the fetch fails] the other holder's lock is removed
n4-cleanup NIT 4: cleanup removes a lock taken over test_a_lock_taken_over_is_left_by_a_sweep_ended_by_a_signal the other holder's lock is removed
c3-2-pid NIT c3-2: a pid line that cannot be written is ignored, and the lock is kept test_a_lock_whose_pid_line_cannot_be_written_is_let_go[the lock directory cannot be written in] the sweep's own pid-less lock is left in the checkout
c3-3-crlf NIT c3-3: a .git file's trailing CR is kept, so its gitdir does not resolve test_a_git_file_naming_a_clones_git_directory_holds_it[CRLF, through a link that resolves once the CR is gone] behaviour
c3-4-beat NIT c3-4: the lock is never refreshed (sc_lock_beat() { :; }) test_the_lock_is_refreshed_before_the_reask the lock reads over ten minutes old at the re-ask, where a writer in another container may take it over
c3-4-relative NIT c3-4: a relative gitdir: is not read against the directory its .git file is in test_a_git_file_naming_a_clones_git_directory_holds_it[relative to the directory it is in] behaviour
c4-1-runner NIT c4-1: the runner's former-name checkout is kept with the reason it had before the fix (the former-name prefix, then the runner's own checkout's reason) test_a_former_name_of_the_runners_own_lane_is_kept_for_its_own_reason[dry run] reason only: nothing is kept or removed differently, the case asserts the whole row (below)
c4-2-rm NIT c4-2: the pid file a failed write leaves is not removed, so the rmdir of the lock fails and the lock is left test_a_lock_whose_pid_line_cannot_be_written_is_let_go[the pid file's write fails, as on a full disk] behaviour: the lock is left holding the empty pid file (['pid']); the case's other parameter alone does not kill it (below)
l3-351-kept MUST 351: a hand edit lanes-edit.sh kept in .git/lanes-edit/ does not hold test_a_hand_edit_lanes_edit_kept_holds[the kept edit is named] behaviour
l3-351-scratch MUST 351: the writer's own scratch in .git/lanes-edit/ holds too (the filter dropped) test_lanes_edit_scratch_alone_holds_nothing behaviour: the writer's own scratch alone holds the checkout
l3-371-mount SHOULD 371: a file system mounted in a checkout does not hold test_a_mount_inside_a_checkout_holds[at an ignored path] behaviour
l3-371-none SHOULD 371: no mount table that can be read holds nothing test_a_mount_table_that_cannot_be_read_holds[no table] behaviour
l3-371-rescan SHOULD 371: the mount table is not read again under the lock test_a_mount_made_after_the_listing_holds behaviour
l3-371-escape SHOULD 371: the path is not spelled with mountinfo's escapes test_a_mount_in_a_checkout_whose_own_path_has_a_space_holds behaviour
l3-372-exec-rm SHOULD 372: the aside's rm is a plain rm again, so a Ctrl-C to the group stops it halfway test_a_signal_during_a_removal_lets_it_finish_and_prints_the_record[INT to its process group, as a Ctrl-C] behaviour: a Ctrl-C to the group leaves the other removal PARTLY done
l3-372-byhand SHOULD 372: a signal after removals prints no NOTED line by hand test_a_signal_during_a_removal_lets_it_finish_and_prints_the_record[TERM to the sweep alone] the remedies: no NOTED line to write by hand after a signal
l3-372-leftover SHOULD 372: a leftover aside is counted with the hidden entries again test_a_removal_left_unfinished_is_named_by_every_run behaviour: the aside a removal left is not listed
l3-372-noting SHOULD 372: a signal during the NOTED write is not known as one test_a_signal_while_the_noted_line_is_written_never_writes_it_twice wording: a signal during the write does not say the line may be on origin already
l3-372-partial-first SHOULD 372: the unfinished removal is not said before the NOTED write test_a_removal_that_does_not_finish_is_said_when_the_noted_write_fails wording: the unfinished removal is not said when the NOTED write fails
l3-373-unmoved SHOULD 373: a push refused with origin unmoved is not told apart test_noted_line_kept_unpublished_is_published_not_written_again the remedies: three handed out where the publish act alone is right
l3-373-restore SHOULD 373: a failed git add or commit is not told apart test_noted_line_whose_commit_fails_is_put_back_then_written_by_hand wording and remedies: the failed commit is not said, nor its restore handed out
l3-373-archive SHOULD 373: a write that lost the race to its bound is not told apart test_noted_line_that_loses_six_push_races_is_archived_then_written_by_hand the remedies: two handed out, with no archive push
l3-373-capture SHOULD 373: the write's words are not kept, so no cause is known test_noted_line_whose_commit_fails_is_put_back_then_written_by_hand wording and remedies: with the words not kept, the failed commit is not said
l3-391-prefix SHOULD 391: the scratch names match as prefixes (lane 3's mutant c) test_a_name_that_only_looks_like_scratch_holds[removed.bak] behaviour
l3-391-link SHOULD 391: a dangling .git/lanes-edit link is not looked at (lane 3's mutant f) test_a_dangling_link_for_lanes_edit_holds behaviour
l3-392-kind NIT 392: a folder or other non-file with a scratch name is scratch (-type f dropped) test_only_a_plain_file_with_a_scratch_name_is_scratch[a folder named removed] behaviour
l3-392-error NIT 392: a find that cannot list the folder holds nothing test_a_dangling_link_for_lanes_edit_holds behaviour
l3-374-under-home SHOULD 374: the move is not asked again whether the path is directly under -lanes/ test_a_lanes_folder_swapped_for_a_link_under_the_lock_holds behaviour
l3-374-mv-lock SHOULD 374: a move that fails leaves the checkout's lock test_a_checkout_that_cannot_be_moved_aside_lets_its_lock_go behaviour: the held checkout's lock is left
l3-374-partly-noted SHOULD 374: a run whose only removal did not finish writes no NOTED line test_a_removal_that_does_not_finish_exits_1[alone] behaviour: no NOTED line for a run whose only removal did not finish
l3-545-unread finding 545: a last-session that could not read is taken as one that names none test_a_last_session_that_cannot_be_read_is_never_none[exit 1, a log that cannot be read] exit code, 2 for 1: a log that could not be read is taken as naming no session
l3-352-tmpdir SHOULD 352: $TMPDIR is not in the dependents scope test_a_dependent_anywhere_in_the_scope_holds[in TMPDIR] behaviour
l3-352-depth SHOULD 352: W's parent is scanned to depth 3, not 6 test_a_dependent_anywhere_in_the_scope_holds[five levels under W's parent] behaviour
l3-352-fail SHOULD 352: a dependents scan that cannot finish holds nothing test_a_dependents_scan_that_fails_holds_every_candidate behaviour: the dry run exits 3, the candidate removable
l3-378-order NIT 378: the lines are read back dependent first test_a_dependent_anywhere_in_the_scope_holds[whose own path holds a TAB] reason: the target is named as the dependent (still held)
l3-356-gitfile NIT 356: a .git file that cannot be read is skipped test_a_dependents_file_that_cannot_be_read_holds_every_candidate[.git file] behaviour: the dry run exits 3, the candidate removable
l3-d37-beside D37: a clone beside W is no candidate test_clones_beside_W_are_removed_leaves_first behaviour: the clones beside W are no candidates
l3-d37-xrepo D37: the clone beside W the caller's LANES_REPO names is not kept test_the_clone_beside_W_the_callers_LANES_REPO_names_is_kept the keep is gone (another hold stops the removal)
l3-d37-still D37: the lane's state is asked of a clone beside W, which is no lane's test_clones_beside_W_are_removed_leaves_first behaviour: the clones beside W are held under the lock, none removed
l3-d37-repo D37: a window whose LANES_REPO names a clone does not hold it test_a_window_whose_LANES_REPO_names_a_clone_beside_W_holds_it[its own path] behaviour
l3-d37-real D37: a LANES_REPO is matched as spelled only, not by its real path test_a_window_whose_LANES_REPO_names_a_clone_beside_W_holds_it[a link to it] behaviour
l3-d37-noproc D37: with no /proc a clone beside W is not held as cannot tell test_with_no_proc_whether_a_window_names_a_clone_beside_W_cannot_be_told behaviour
l3-d37-aside D37: a clone beside W is moved aside into -lanes/ test_a_removal_beside_W_that_does_not_finish_leaves_its_aside_beside_W behaviour: the aside is made in <W>-lanes/, not beside W
l3-d37-leftover D37: an aside beside W is not named by later runs test_a_removal_beside_W_that_does_not_finish_leaves_its_aside_beside_W behaviour: the aside left beside W is not listed
l3-d37-noted D37: the NOTED line does not name the clones beside W it removed test_clones_beside_W_are_removed_leaves_first wording: the NOTED line lacks the clones beside W
l3-d37-names D37: a clone's name is written into the NOTED line as it is test_clones_beside_W_are_removed_leaves_first wording: a name with a blank is written as it is
l3-394-wide NIT 394: a comment line of the sweep's section over 80 columns test_the_sweeps_comments_are_80_columns_or_less the width case: a comment line at 88 columns
l3-377-enter NIT 377: a -lanes/ that cannot be entered or listed is not refused test_a_lanes_folder_that_cannot_be_entered_or_listed_is_refused exit code: the run reads on past a <W>-lanes/ it cannot enter
l3-376-w NIT 376: --yes under routing with a LANES_REPO that is W is not refused test_yes_with_LANES_REPO_naming_W_is_refused_before_anything_is_removed[W] exit code: the act ran and removed
l3-359-colon NIT 359: the pid line's container drops lock_ident's : test_the_locks_pid_line_is_lanes_edits_own[a container with a colon] wording: the pid line's container
l3-379-long NIT 379: the aside's name keeps the whole name, past NAME_MAX for a long one test_a_long_name_is_moved_aside_under_a_name_that_does_not_grow behaviour: the long name is held, as it cannot be moved aside
l3-358-keep NIT 358: a keep line prints its path raw test_a_name_with_a_control_character_writes_no_line_of_its_own behaviour: a forged row is printed
l3-354-reflog NIT 354: commits only the reflog reaches are not named test_commits_only_the_reflog_reaches_are_named wording: the reflog's commits are not named
l3-355-repo NIT 355: a repository at an ignored path does not hold test_what_ignored_rules_hide_that_may_be_work_holds[a repository at an ignored path] behaviour
l3-355-swap NIT 355: an ignored editor's swap file does not hold test_what_ignored_rules_hide_that_may_be_work_holds[an ignored swap file] behaviour
l3-355-tag NIT 355: annotated tags are not named test_annotated_tags_are_named wording: the annotated tag is not named
l3-375-lock NIT 375: a second --yes takes the sweep-wide lock anyway (mkdir -p) test_a_second_sweep_is_refused_while_one_runs exit code, 0 for 2: a second sweep runs
l3-375-gone NIT 375: a candidate gone at its turn is held, not said as gone test_a_candidate_gone_at_its_turn_is_said_as_gone reason: a candidate gone at its turn is held, not said as gone
l3-353-cwd NIT 353: a process that came in after the last read is not asked of the aside test_a_process_that_comes_in_after_the_last_read_moves_it_back behaviour
l3-353-deps NIT 353: a clone made after the last read is not asked after the move test_a_clone_made_after_the_last_read_moves_it_back behaviour
l3-353-text NIT 353: a .git or alternates target that is gone is dropped, not kept test_a_clone_made_after_the_last_read_moves_it_back behaviour
l3-353-pid NIT 353: the pid line is not read again from the aside test_a_lock_taken_over_at_the_move_moves_it_back behaviour
l3-353-back NIT 353: a checkout held after its move is not moved back test_a_lock_taken_over_at_the_move_moves_it_back behaviour: the held checkout is left at its aside
rv4b-A-refuse MUST A: --yes with a LANES_WORKSPACE_ROOT that is not path:'s W is not refused test_a_W_override_naming_a_clone_beside_W_never_sweeps_the_real_W[--yes] behaviour: --yes is not refused, and the run sweeps from the wrong W
rv4b-A-real MUST A: the override is compared with path: as spelled, not by its real path test_a_W_override_naming_W_itself_is_no_refusal[a link to W] exit code, 2 for 0: an override through a link to W itself is refused
rv4b-A-path MUST A: the directory workspace.yaml's path: names may be a candidate test_the_directory_workspace_yaml_names_is_never_a_candidate behaviour
rv4b-A-lanes MUST A: a clone beside W with a -lanes/ beside it may be a candidate test_a_clone_beside_W_with_lane_checkouts_of_its_own_is_never_a_candidate[at the listing] behaviour
rv4b-A-relock MUST A: whether it is a W is not asked again under the lock test_a_clone_beside_W_with_lane_checkouts_of_its_own_is_never_a_candidate[after the listing] behaviour
rv4b-A-dry MUST A: the dry run does not say what --yes refuses test_the_directory_workspace_yaml_names_is_never_a_candidate wording: the dry run's header does not say that --yes refuses
rv4b-D-repo SHOULD D: a window that came to name the old path does not move it back test_a_window_that_names_it_after_the_last_read_moves_it_back behaviour
rv4b-D-repo-scan SHOULD D: the windows' environments are not read again after the move test_a_window_that_names_it_after_the_last_read_moves_it_back behaviour
rv4b-D-reponone SHOULD D: with no /proc after the move a clone beside W is not moved back test_a_proc_gone_as_a_clone_beside_W_is_moved_aside_moves_it_back behaviour
rv4b-D-mount SHOULD D: a mount that came to be in the aside does not move it back test_a_mount_made_as_it_is_moved_aside_moves_it_back[a mount in it] behaviour
rv4b-D-mount-scan SHOULD D: the mount table is not read again after the move test_a_mount_made_as_it_is_moved_aside_moves_it_back[a mount in it] behaviour
rv4b-D-mountnone SHOULD D: no mount table after the move does not move it back test_a_mount_made_as_it_is_moved_aside_moves_it_back[no table] behaviour
rv4b-D-aside SHOULD D: the mounts are matched against the old path, not the aside that carried them test_a_mount_made_as_it_is_moved_aside_moves_it_back[a mount in it] behaviour
rv4b-B-own SHOULD B: a directory of this user's own the scan cannot read holds nothing test_a_directory_of_this_users_own_the_dependents_scan_cannot_read_holds_every_candidate[plain] behaviour
rv4b-B-any SHOULD B: every directory the scan cannot read holds, another user's too test_a_directory_of_another_user_the_dependents_scan_cannot_read_is_counted_and_holds_nothing behaviour: another user's unreadable directory holds every candidate
rv4b-B-quotes SHOULD B: find's quotes are kept on the path, so no directory is told to be this user's test_a_directory_of_this_users_own_the_dependents_scan_cannot_read_holds_every_candidate[plain] behaviour
rv4b-B-unquote SHOULD B: find's escaped quote is kept, so a name with a quote is not found test_a_directory_of_this_users_own_the_dependents_scan_cannot_read_holds_every_candidate[with a quote, a blank and a backslash] behaviour
rv4b-B-escapes SHOULD B: find's backslash escapes are kept, so a name with a backslash is not found test_a_directory_of_this_users_own_the_dependents_scan_cannot_read_holds_every_candidate[with a quote, a blank and a backslash] behaviour
rv4b-C-depth SHOULD C: TMPDIR is scanned to depth 7, short of a clone eight levels down a scratchpad test_a_dependent_anywhere_in_the_scope_holds[in a Claude Code scratchpad, eight levels under TMPDIR] behaviour
rv4b-C-scope SHOULD C: the scope line names a depth that is not the scan's test_the_dry_run_names_the_dependents_scope wording: the scope line names a depth the scan does not use
r4b-home-depth rv4b's mutant: $HOME is scanned to depth 0 test_a_dependent_anywhere_in_the_scope_holds[in HOME] behaviour
rv4b-353-text the 353 gap: a target that does not resolve is kept as written, its .. and all test_a_relative_path_written_after_the_last_read_moves_it_back[.git file] behaviour
rv4b-353-gitfile the 353 gap: a .git file's target that does not resolve is dropped test_a_relative_path_written_after_the_last_read_moves_it_back[.git file] behaviour
rv4b-353-alt the 353 gap: an alternates target that does not resolve is dropped test_a_relative_path_written_after_the_last_read_moves_it_back[alternates file] behaviour
r4b-rel-alt rv4b's mutant: a relative alternates line is not read against its objects directory test_a_relative_path_written_after_the_last_read_moves_it_back[alternates file] behaviour
w4r-353-post-gitfile D46: a .git file written after the last read is not read after the move test_a_git_file_written_after_the_last_read_moves_it_back behaviour
w4r-353-post-lines D46: the read after the move matches the alternates lines twice, the .git files' never test_a_git_file_written_after_the_last_read_moves_it_back behaviour
rv4b-G-nested NIT G: a move back into a directory that came is said as moved back test_a_directory_made_at_its_path_as_it_is_moved_back_is_said reason: "it was moved back" over a checkout hidden in the new directory
rv4b-I-refusal NIT I: the refusal of a lane says the sweep looks under -lanes/ only test_the_sweep_takes_no_lane_and_no_other_act[a lane] wording: the refusal of a lane
rv4b-J-line NIT J: the header does not name the container whose processes were read test_the_dry_run_names_the_dependents_scope wording: the header's processes line
rv4b-J-dry NIT J: the dry run does not read the container and host test_the_dry_run_names_the_dependents_scope wording: the header's processes line names no container
w4r-D49-claim-reads D49: a claim's whole Rule 1 reads in .git/lanes-edit/ hold the checkout test_a_claims_whole_reads_alone_hold_nothing behaviour: a leftover claim-reads file holds the checkout for good
w4r-D49-prefix D49: anything with claim-reads- in its name is scratch, not only a name starting so test_a_name_that_only_looks_like_scratch_holds[xclaim-reads-1.txt] behaviour

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant