You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
lane-worktrees report, lanes-edit.sh, tests/run.sh, lane-start: #170's honesty, portability and test items (#170, part 2) - #179
This is part 2 of #170, stacked on #174, so its base is feat/lane-worktrees-residue. #174 takes every item that can destroy or publish something, or that decides a gate. This PR takes the rest of #170: report honesty, portability, the suite wrapper, the daily start, test honesty and nits. With both landed, nothing in #170 is left open, so this PR says Closes#170.
Items 3, 9, 10 and 11 were not in #170's PR 2 list. They are taken here so that "Closes" is true. (d) and B8 were begun by lane openRepoTools-3's writer on a local branch that was never pushed. They were finished and proved here.
Items, commits and tests
"Red" means the case failed with the base's (499d2ab, #174's head) lane-worktrees, lanes-edit.sh, lane-start, AGENTS.md and tests/run.sh, run with this branch's tests, in proof run 37507138793, job red.
Passing at the base by design. C1, C3, C4 and C6 are test-honesty items. Each makes a case assert what it claimed to prove, and the base's code already behaves correctly there. So 7 of the 29 selected cases pass at the base, and that is the point: they now pin that behaviour positively instead of vacuously.
C1 now asserts the remote branch is kept.
C3 reads the start's own synchronous word instead of sleeping.
C4 uses a git that checks origin at the moment of worktree remove.
C6 uses a pid it started, reaped and proved gone.
Copilot round 1 on this PR
There were three threads, all real defects in this PR's own code, and each is fixed with a case. "Red" means the case failed with lane-worktrees and lane-start at 0292005 (the head before the round) and this branch's tests, in proof run 37513097772. At e66195c in that run, the lane_worktrees, run_wrapper, pathspec_check, repo_hygiene and openrepotools_command cases pass (376). The one failure is the macOS-job hygiene test, which reads the trimmed proof workflow itself.
test_lane_worktrees.py::test_the_hand_written_register_line_is_quoted_for_the_shell (now wants the session word)
yes
Copilot round 2 on this PR
Review 5433148700 on e66195c opened no inline threads. Its two "previously missed" findings are real defects, and both are fixed. "Red" means the case failed with lane-worktrees and tests/run.sh at e66195c, in proof run 37514530247. At 7ac5cbe in that run the same selection passes (378); the one failure is the macOS-job hygiene test, which reads the trimmed proof workflow.
yes (20.7 s: the scan waited out its deadline twice)
Design notes
G14. (The scan runs in the wrapper's own shell and passes over its own children since 7ac5cbe.) A non-interactive bash never hands a job the terminal. Measured: set -m with fg left the suite stopped on SIGTTIN. So the wrapper reads its process group's members before the suite starts. Afterwards, whatever is in the group that was not there before, and is not a sibling the caller started, is TERMed and then KILLed after ten seconds, before the cleanup.
E8. The report runs under nohup, and under setsid where there is one. Linux has it; macOS does not, and there nohup alone keeps the hangup away.
E9.AGENTS.md stays at its 316-line cap: the one line changes in place, and a hygiene assertion holds it there.
Verification
tests/run.sh on the workstation was starved by other projects' suites, so CI is the record. Proofs ran on temporary proof/openRepoTools-1/* branches with a trimmed tests.yml, each deleted once read:
head (-k 'lane_worktrees or run_wrapper or pathspec_check or repo_hygiene'): 291 passed and 1 failed. The failure is the macOS-job hygiene test, which reads the trimmed proof workflow itself.
red: 22 failed and 7 passed (the by-design cases above).
parse-macos: bash 3.2 parses every shipped bash file.
helpers: the shell suite under plain python3 -m pytest -k lane_helpers, as CI runs it. 22 passed.
helpers-base-runsh: the BASE tree's shell suite through tests/run.sh. 4439 ok and 1 FAIL, "the row's stamp states clause (c)'s two facts for a person, in its TAIL". That failure is the base's own, not this PR's. Under run.sh's long run root, the lane row's stamp is cut before its dir …; window … tail. The pre-rebase proof saw the same failure on this branch through run.sh. It is not touched here; it is reported to the lane.
An earlier run, 37501364279, on this branch's pre-rebase head over 685cbe3, gave the same head and red results.
This PR's own CI is the landing record. I have not applied ready.
Harden lane reporting, sweeping, daily startup, and test execution for honest failure handling, portability, privacy, and reliable cleanup.
Bug Fixes:
Make lane-worktrees reporting accurately surface unread state, register, filesystem, and evidence data without treating missing information as clean results.
Improve sweep safety by preserving trees when liveness is unknown, protecting symlink targets, finding pull requests across all branch histories, and emitting complete shell-safe register commands.
Make daily reports private, retryable when permissions cannot be secured, and resilient to terminal hangups.
Harden the test wrapper against TERM-resistant suites, caller-supplied temporary directories, and child processes left behind by terminal runs.
Fix bytecode path checks across locales and non-ASCII filenames, and quote offered .gitignore repair commands.
Enhancements:
Expand report coverage to discover lane state and evidence throughout nested estate layouts while remaining non-mutating and portable across older Git versions.
Replace timing-based report tests and unreliable fake dead PIDs with positive behavioral assertions.
Documentation:
Update contributor and lane documentation for clean test execution, session-aware sweep records, comprehensive report discovery, report privacy, and process cleanup behavior.
Tests:
Add regression coverage for report honesty, portability, filesystem and encoding failures, sweep ordering and safety, shell path handling, daily report lifecycle, and test-wrapper cleanup.
A swap that does not finish gracefully leaves its writers' trees where
they stood, and nothing removed them: a person archived, verified and
removed them by hand. `lane-worktrees sweep <lane>` makes that one act,
dry run by default, under one rule: nothing is deleted that is not
first on origin or in a bundle under the sweeps directory, and a live
writer's tree is never touched.
Which trees are the lane's is #97's inventory and the disk, never the
derived index (Amendment 14(b)); a tree the inventory does not name is
FOREIGN and is left unless --include-foreign and a --word, and one
another lane's inventory names is never taken. Who may act is #97's
reconciliation, read before any write including the fetch: a lane bound
elsewhere, held by another live session, managed-owned or unreadable is
refused (exit 2); a lane this session holds acts only on the writer
count the coordinator states with --live.
"Merged" is the register's LANDED line for the branch's PR or gh's
MERGED for it, never ancestry alone. Unpublished commits are pushed
under the branch's own name, never its upstream's (a branch made from
origin/main tracks main), or to rescue/<lane>/<slice>-<UTC> where
origin diverged; dirty work becomes a WIP commit built through a copy
of the index, so a refused push leaves the tree exactly as it was.
Removal re-proves the head and the content at the moment it happens.
Also --branches, --include-scratch, --include-caches,
--include-sandboxes, --links, alternates and local-remote detection
(LOAD-BEARING clones are never removed), sweep --expire with 90-day
retention that never expires an archive whose rescue left origin, and
the porcelain contract lane-end's gate (#163) reads: 0 nothing to
retire, 3 something to retire, 2 refused.
Part of #162.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/test_lane_worktrees.py builds a lane's estate under tmp_path: a
bare origin reached through a url.insteadOf for a GitHub-shaped URL, the
lane's checkout and both worktree roots, and fakes for lanes-edit.sh,
gh and tmux. One lane holds a tree in every state, and the dry run is
held to the table row for row and to a whole-estate snapshot that does
not move. The same lane under --yes is held to every act: pushes,
rescue branches seen on origin, the WIP commit's subject and parent,
the bundles, the ignored-file archive without bytecode, a manifest
that verifies, and one register line per tree.
Also: a refused push leaves the dirty tree byte for byte; a live writer
(a process's cwd, a tmux pane) is never touched; this session's --yes
needs its writer count; bound elsewhere, held elsewhere and unreadable
are refused with 2 and change nothing; merged by register and by gh
against a branch on main by ancestry alone; no gh, nothing merged;
--include-foreign takes a word and never another lane's tree;
--branches; scratch, caches and sandboxes; --links; a LOAD-BEARING
clone; --expire at 89/90/120 days; the porcelain exit codes; and two
cases on the REAL lanes-edit.sh, for the inventory, the register line
and a lane bound on another host.
Part of #162.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`openRepoTools --install` places `lane-worktrees` at the end of
INSTALLABLES, so every index a test takes still names the file it did.
The file count moves 16 -> 17 and the artifact count 30 -> 31 wherever
they are stated, with the count history extended rather than restated;
the receipt sentence now says 31 artifacts, 29 rows. --help names the
new command.
The hygiene suite compiles `lane-worktrees` beside `lanes-index`: the
two shipped commands that are Python and have no .py suffix for the
glob to find.
Part of #162.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Retiring a lane's worktrees": the rule, which trees are the lane's and
who may act, the disposition table as implemented in the order it is
decided, the other rows (--branches, scratch, caches, sandboxes, links,
bundles), the sweeps directory and its manifest, the one register line
per tree, retention and sweep.conf, the porcelain exit contract for
lane-end's gate, and the two protocol lines the act assumes, proposed
for the amendment that ratifies it.
Part of #162.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`os.walk` lstat()s every entry, and the estate on Eagle is tens of
thousands of directories: a read-only `--links` dry run took 245 s.
A scandir walk reads each entry's kind from the directory read itself,
so only a symlink or a `.git` file costs a call of its own. Measured
on the same estate: 16.7 s, with the same 403 broken links found (99
of them worktree gitdir pointers).
Part of #162.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A suite run left tests/__pycache__, .pytest_cache and, when killed, its
sandboxes in whichever worktree ran it; the estate cleanup found those
were 968 of 1,000 removals (#162, causes 3 and 4). tests/run.sh now
sends bytecode to ${XDG_CACHE_HOME:-~/.cache}/openRepoTools/pycache,
turns pytest's cache off, and roots --basetemp and TMPDIR under one run
root in ${XDG_STATE_HOME:-~/.local/state}/openRepoTools/tmp/<UTC>-<pid>/
that an EXIT trap removes however the run ends.
The suite now runs in the background and is waited for: bash runs a trap
only once its foreground child returns, so a TERM used to wait out the
whole suite. The INT and TERM handlers stop the suite, then exit, so the
EXIT trap removes the run root and, on the mkdir path, the lock. The
lock is computed before TMPDIR moves, so it stays the workstation's.
The repository venv is used when it has pytest, first on PATH so the
command line still reads `python3 -m pytest`.
tests/test_run_wrapper.py proves it against a stub suite: the
relocation, the run root gone after a pass, a failure, a TERM and an
INT, under flock and under the mkdir lock, and the venv.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One virtual environment per repository, outside the estate, is the
convention the sweep's --include-caches row assumes (#162, cause 3): a
venv/ inside a worktree is a leftover to remove. --install now names
this repository's, ${XDG_CACHE_HOME:-~/.cache}/openRepoTools/venvs/
openRepoTools, and says whether it is there; when it is not, it prints
the two commands that make it. It never makes one: that needs pip and
the network, and an install from a checkout needs neither.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cause 19 of the cleanup analysis: a handoff's attachments were committed
with a whole virtualenv's __pycache__ inside them, and brett-wip carries
703 bytecode paths in its history. `wip init` now adds __pycache__/,
*.pyc, *.pyo, .pytest_cache/, .mypy_cache/, .ruff_cache/, node_modules/,
.venv/ and venv/ to the seeded .gitignore, in wip_substitute - the one
function both step-7 paths go through - so the bytes a seed writes and
the bytes a re-run compares are the same. A line the template already
carries is not repeated, so a template that adopts them upstream seeds
them once.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The .gitignore lines keep a new workspace clean; an older one without
them still stages whatever a pathspec names. commit_push now asks git
what its pathspec would stage (git add --dry-run, which honours
.gitignore exactly as the real add does) and refuses, exit 2, nothing
staged, when any path has a __pycache__, cache, node_modules, venv or
site-packages component or is *.py[co] - and offers the .gitignore
lines that workspace lacks, with the one command that adds them.
Attachments are committed by hand, so the same question is a
subcommand: `lanes-edit.sh pathspec-check <path>...` answers 0 clean,
2 with the offending paths on stdout and the offer on stderr, 64 usage.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The report is the net under every actor that never runs lane-end, and
it only works if it is read every day. lane-start now runs
`lane-worktrees sweep --all --dry-run --report` once per UTC day per
workstation, after the row is written and before the launch: the first
start of the day takes report-<YYYYMMDD>.stamp under the state
directory with an atomic `set -C` create, removes older stamps, and
starts the report detached, stdin, stdout and stderr closed, writing
reports/<UTC>.md. Every step either works or is skipped in silence, so
the report never delays a start and never fails one. --dry-run starts
none; LANE_WORKTREES_REPORT=off is the switch, and the suite sets it.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Workspace creation outpaces closeout, and nothing counted it. The
report reads every lane's #97 inventory and snapshot (beside each
checkout's parent, where #97 keeps them), the workspace register and
the disk - never the derived index - and lists, as one markdown
document: FOREIGN clones with size, last commit and Lane: owner;
orphaned trees of ENDED lanes; unmerged branches with a missing,
diverged or unpushed upstream; root-main divergence, with Amendment
4's remedy where only handoff/register paths moved; rescue branches
and dirty inventory trees awaiting disposition past aging_days; caches
and sandboxes by size; ignored directories over ignored_report_mb;
evidence-shaped paths; archives past retention (--expire's own table,
now shared as expiry_rows); the workspace's .gitignore and bytecode
history; and `status --all`'s findings, reported as what they are and
never counted as dirt. The head table carries the rescue-branch count
and the sweeps directory's size.
It changes nothing: no fetch, no index refresh, no expiry. --post
writes it to a file or comments it on owner/repo#n through gh.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lanes manual gains the report (its sections, --post, the daily run
from lane-start and its switch) and what keeps leftovers from arriving:
the run.sh relocation, one venv per repository outside the estate, one
evidence root per repository under the state directory, and the
workspace's bytecode rules with lanes-edit.sh's refusal and
pathspec-check.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every one of these let a destructive act rest on a read that was stale,
failed, or answered a different question:
- Merged now needs the PR's base to be the default branch (origin's HEAD,
else main/master): a merge into a release branch is not a landing. An
OPEN PR protects its branch before any older merged PR on that branch
counts, unless the register LANDED that very PR.
- A failed `rev-list` distance is a read error, never "0 ahead", for a
tree and for --branches' upstream state alike.
- A clone whose git directory linked worktrees share is kept. Removing it
would take their repository with it.
- A tree that another lane's inventory also names is kept. An inventory
record that cannot be read refuses the sweep (exit 2) instead of being
skipped. The lane name goes through `canon-lane` before any path is
derived from it, and an unreadable alias table refuses.
- Liveness is asked again before the rescue and again just before
removal, afresh (processes, tmux, the recording session). A scan that
cannot be made leaves the tree. Scratch is removed only if no writer
arrived and nothing outside its caches changed while the tar ran. A
cache is rechecked against its owning tree.
- Caches never come from a FOREIGN tree, a live one, or one whose
liveness is unknown.
- Branches are deleted at the SHA that was judged: `update-ref -d <old>`
locally, a --force-with-lease push for the remote. Each failure is
counted, and the register claims "+ remote" only for a remote delete
that happened.
- --branches fetches the lane's own checkout too, and a failed fetch
deletes nothing.
- A stale registration is removed by `git worktree remove <path>` alone,
never by a repository-wide prune.
- An ignored-file listing that failed stops the act. It is never read as
"nothing ignored".
- Each invocation gets its own archive directory, created exclusively.
- An origin-less worktree under --bundle --yes is bundled and removed,
as its dry run says, instead of being read as a failed fetch.
- --expire requires the rescued SHA itself on origin and an archive that
is whole: a MANIFEST.sha256 listing every file at its digest, and a
rescues.tsv whose rows parse.
Ten new or extended cases. Each one fails against the previous head and
passes here.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The manual now states what changed: the canonical lane name, refusal on
an unreadable record, contested trees, the single-registration prune,
the linked-worktree clone guard, merged-into-default with open PRs first,
SHA-fenced branch deletes, liveness asked again before removal, scratch
and cache rechecks, the exclusive archive directory, and expiry of whole
archives only.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The merge from #168 brought in archive_ledger and the exact-SHA check.
The report's archive section reads them through expiry_rows, so its
docstring now says it keeps archives that are not whole and those whose
rescue branch moved.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot on #169: a TERM to the pytest pid alone leaves whatever it is
waiting on (a shell suite, a git, a fixture's sleep) running, while the
EXIT trap deletes its temp root and releases the mkdir lock around it.
The suite now starts in a process group of its own, with `set -m` on for
that one line only. The INT/TERM handlers and the EXIT trap TERM the
whole group, reap the leader, wait up to ten seconds for running members
and then KILL the group. Running members are counted from `ps`, never
`kill -0`, because an orphan's zombie answers `kill -0` until whoever
adopted it reaps it.
The kill cases now give the stub suite a child of its own and require
it to be gone. Against the previous run.sh that case fails with "the
suite's own child outlived its wrapper".
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot on #169: the refusal classifies any path under a site-packages
directory, since that is a virtual environment under any name, but
neither the offered lines nor the seeded ones covered it. Following the
offer therefore left the refusal standing. `site-packages/` now joins
BYTECODE_IGNORES and WIP_HYGIENE_LINES. The shell case adds a venv
named env-x: it is refused, `site-packages/` is among the lines offered,
and it is still refused until that line is added.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot on #169, two report defects:
- Two clones of one origin, neither named for the repository, were both
treated as canonical, so the duplicate finding vanished. The checkout
is now chosen deterministically (the one named for the repository,
else the shallowest), and every other clone is reported, with a note
when the choice was ambiguous.
- `status --all` was pointed at the estate only when --estate was given.
It now always gets the root the report resolved, and lane-start passes
its own $PROJECTS_ROOT to the daily run.
The report's hygiene check wants site-packages/ too. Each new assertion
fails against the previous head.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seven of round 2's eight. Each one let an act proceed on an unknown:
- Another lane's inventory record or directory that cannot be read, or
that names no path, now refuses the sweep. Who else claims a tree is
read before the fetch, so the refusal writes nothing.
- A submodule's ignored files (an .env) keep the tree. The tree's
ignored archive covers the superproject only.
- Ignored files are printed before the archive and listed again at
removal. A change, an addition or a failed listing leaves the tree.
- A prune candidate whose directory reappeared is left for the next
sweep to classify.
- A pytest sandbox's .lock is read again at the act, and a live pid
leaves it.
- `tracked()` returns None when a repository's index cannot be read,
and such a cache is left.
- A manifest entry whose file is gone makes the archive not whole.
The eighth, unreadable /proc entries of same-account processes, is a
design trade and is filed in #170. On Eagle, 9 such processes exist at
any time (6 `timeout`, 3 `ssh-agent`), so reading them as "unknown"
would keep every tree on every sweep.
Four new or extended cases. Each fails against ae17742 and passes here.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot round 2 on #169: `git add --dry-run` says nothing about a path
the index already holds at those bytes, yet the path-limited commit
still takes it. bytecode_in_pathspec now also reads `git diff --cached
--name-only --diff-filter=d` for the same pathspec. A staged deletion of
bytecode is a cleanup and is let through. The shell case force-stages a
.pyc after the ignore lines are in place: pathspec-check and the commit
path both refuse it, and nothing is committed.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot round 2 on #169: the command is meant to be pasted into a
shell, and $XDG_CACHE_HOME is the person's to choose, spaces included.
Both paths are now `printf %q`-quoted. The new case installs with a
cache directory containing a space, and shlex reads each path back as
one word.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot round 2 on #169: an unreadable lane snapshot or inventory
record became an empty one, so a lane or tree dropped out of every
section and the report could look clean. Reads now go through _ls and
_record. A failure other than "not there", a tree record naming no
path, or an inventory tree whose git status fails is a row in a new
section, "Records the report could not read". The unused _sidecar
reader is gone.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_repo_hygiene caps AGENTS.md at 316 lines and README.md at 486, and
both files were exactly at their caps on main. The run.sh paragraph had
taken AGENTS.md to 330 and the venv sentence README.md to 491. The full
text already lives in docs/README-lanes.md, so AGENTS.md now says the
same thing in the three lines the old sentence took, and README.md says
it on the line it extends. No cap is raised.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot on #169: macOS has no flock, so the wrapper takes the mkdir lock
and never creates openrepotools-pytest.lock. The relocation case checked
for that file unconditionally and would have failed the macOS job, which
is the landing gate. Where there is no flock, the case now checks
instead that the mkdir lock was released.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The adversarial review of 3c0b188, reproduced with its probes against
the suite's own Estate fixture:
1. --yes and --expire --yes are refused, exit 2, changing nothing,
unless LANE_WORKTREES_ENABLE_YES=1 is set. #170 lists data-loss paths
that are still open. The suite's Estate sets the variable; a real
estate should not until #170 lands. The dry run, --porcelain and
--report are untouched.
2. B1: a tree that #97's inventory does not name is still the lane's if
it stands under .lane-worktrees/<lane>/, or if it sits in
<checkout>/.claude/worktrees on a branch whose own commits carry this
lane's Lane: trailer. Another lane's claim still wins. Such trees keep
the quiet-hours liveness rule. A lane whose state is NONE but which
has trees of its own is refused, exit 2. On Eagle, `sweep
openRepoTools-3 --dry-run --porcelain` gave exit 0, `summary 0 15 0
15`. It now gives exit 2, `summary 9 13 4 4`: 9 trees are the lane's,
and the snapshot is NONE.
3. B2: subprocess output and git metadata files are decoded with
surrogateescape, and stdout and stderr write with it too, so a
Latin-1 worktree name is reported rather than raising. An unreadable
pytest-of-$USER is a kept sandbox row. A catch-all keeps errors that
no read caught inside the contract: exit 2 on a dry run, and under
--yes DISPOSITION.md is written first and the exit is 1.
4. B3: every porcelain field escapes backslash, TAB, newline and CR.
Worktree registrations are read with `git worktree list -z` where git
has it, so a newline in a path no longer becomes a phantom
registration.
Five new cases (switch off, ownership plus NONE, escaping, non-UTF-8
path, unreadable sandbox root), each failing against 3c0b188. Two cases
were moved off the lane root to keep testing FOREIGN trees. 40 cases.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The manual now leads the sweep section with the --yes switch and #170.
It also says which unrecorded trees are a lane's and that a NONE snapshot
with trees is refused. The exit contract now covers the escaped fields
and the catch-all.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adversarial review B7: `set -m` puts the suite in a background process
group with the terminal as its stdin. Anything that reads the terminal
(--pdb, breakpoint(), input()) is then stopped by SIGTTIN, and the
wrapper waits on it for ever while holding the workstation flock. With
a terminal on stdin the suite now runs in the foreground, in the
terminal's own process group, and a Ctrl-C reaches all of it from the
terminal. Without a terminal the process-group stop is unchanged.
The new case runs the wrapper on a pseudo-terminal whose stub suite
prints a prompt and reads a line. It passes here. Against the previous
run.sh it hangs and fails, with the transcript showing the prompt and
the typed line that was never read.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.
The untracked listing is still treated as complete without checking its status. If this git ls-files fails, its empty or partial stdout silently removes untracked evidence from the report, whereas the ignored listing immediately above correctly records the failed read. Record this failure too and consume stdout only on success.
Exclude scanner processes to avoid guaranteed ten-second delays
tests/run.sh:175
strays sees the ps process (and the command-substitution shell) that it starts in the same terminal process group. Those PIDs were not in TTY_BEFORE, so even with no leftover suite process every call stays nonempty, executes all 50 sleeps, and adds a guaranteed ten-second delay to each terminal run. Capture the scan first and exclude the scanner and direct helper children before applying the baseline filter.
…s a finding, its output never evidence
#179 Copilot round 2 (lane-worktrees `Report._ignored_and_evidence`): the
evidence scan took the untracked `git ls-files` output whatever its exit,
so a listing that failed silently dropped that repository's untracked
evidence - or offered its partial output as the repository's files -
while the ignored listing beside it recorded its own failure. A failed
untracked listing is now a row of "Records the report could not read"
(no lane record, so the orphan and aging sections keep their "none"), and
its stdout is used only on success.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…un that leaves nothing waits for nothing
#179 Copilot round 2 (tests/run.sh `strays`): G14's scan ran in a command
substitution, so its subshell, `ps` and `awk` were members of the
terminal's process group that were not there before the suite. Every scan
found "strays", every terminal run waited out the whole ten-second
deadline after a suite that left nothing, and the pids it signalled were
the scan's own, already gone.
The scan now runs in the wrapper's own shell, writing to a file under the
run root, so its `ps` and `awk` are the wrapper's children (ppid `$$`) and
are passed over; a real stray never is one, because the suite, the
wrapper's child, has returned and what it left was reparented away. The
pids are read back with `read`, no fork. bash 3.2 has no `$BASHPID` to
name a subshell by, which is why the scan is not simply told its own pid.
The terminal case is now a helper, and a second case wants a terminal run
of a suite that leaves nothing to finish in under eight seconds.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot round 2 (review 5433148700 on e66195c). This round opened no inline threads. Its two "previously missed" findings are both real defects, both fixed in 7ac5cbe's push, and each has a case:
lane-worktrees:5113, the untracked listing taken whatever its exit. Fixed in 19e53be. A failed listing is now a row of "Records the report could not read", and its output is never offered as evidence. Case: test_an_untracked_listing_that_failed_is_a_finding_never_evidence.
tests/run.sh:175, the stray scan seeing its own subshell, ps and awk. Fixed in 7ac5cbe. The scan now runs in the wrapper's own shell, writing to a file under the run root, and passes over the wrapper's own children (bash 3.2 has no $BASHPID). Case: test_a_terminal_run_that_leaves_nothing_waits_for_nothing (under 8 s).
Proof run 37514530247 (temporary branch, deleted once read):
Head: 378 passed. The one failure is the macOS-job hygiene test, which reads the trimmed proof workflow.
With lane-worktrees and tests/run.sh at e66195c, both cases fail. The terminal run took 20.7 s, because the scan waited out its deadline twice.
This was the second and last Copilot round for this PR. Lane openRepoTools-1.
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.
…esidue
Main is 500687c, the squash of #168 and #169, so this branch's history
and main's carry the same content under different commits. The merge is
taken against this branch's old base 45fa45d, so main's text stands
wherever #174 did not itself change it; the result differs from main by
exactly #174's own diff.
Lane: openRepoTools-1
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…the name
Review of #179 at 7ac5cbe (lane 3's Opus pass): _latin1_clone in
tests/test_lane_worktrees_report.py cloned into a non-UTF-8 path
(caf\xe9) with no skip. APFS, the macOS runner's, answers a non-UTF-8
name with EILSEQ ("Illegal byte sequence"), so the tests-macos job
would fail test_bytes_that_are_not_utf8_never_abort_the_report before
reaching its assertions.
The helper now makes the raw-bytes directory first, the same pattern
d2f8051 applies to tests/test_lane_worktrees.py: where the filesystem
refuses it (OSError) the case skips with the same wording, "filesystem
refuses non-UTF-8 names". Linux accepts the name, so the case runs there
as before, and git clones into an existing empty directory.
Nothing else changes.
Lane: openRepoTools-1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s lane's (#174 review R1)
A tree in `<checkout>/.claude/worktrees` that no inventory names was
adopted when ANY commit of it not on origin's default branch carried
this lane's `Lane:` trailer. A tree another lane stacked on this lane's
commit - base by repoA-1, tip and an edit in progress by repoB-2 - was
therefore repoA-1's: its `--yes` removed the tree and pushed repoB-2's
edit to rescue/repoA-1/..., and on the real estate
`.claude/worktrees/closeout-2` was adopted by both lanes sharing the
checkout.
`_lane_claims` now also asks `head_lane_trailer`: HEAD's own last
`Lane:` trailer, read as `read_tree` reads `lane_trailer` (which runs
after the claims are decided), must be this lane's. The tree is then
the lane's whose commit HEAD is, and FOREIGN to every other.
The new test builds the reviewer's probe: repoA-1's dry run reads the
tree FOREIGN (exit 0), its `--yes` leaves the tree and its edit and
pushes nothing, and repoB-2's dry run still adopts it by its trailer.
The manual's adoption sentence and the docstring say HEAD's own trailer.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…repository (#174 review R2)
With `--include-sandboxes --yes` an unmarked `/tmp/tmp.*` untouched for
aging_days (14) was removed with no archive. The reviewer's probe: one
tmp.* holding a BARE repository with the only copy of a commit, one
holding notes.txt - both removed, nothing archived. `repository_within`
looked for a `.git` alone, which a bare repository has not, and
tests/run.sh writes its marker in its own run roots, never in a tmp.*,
so the 14-day rule was the only path that removed real ones.
#170 item 1, option (b), as the issue words it: list `tmp.*` and never
remove it, so only `pytest-of-$USER/pytest-*` with a dead `.lock` pid
and tests/run.sh's own `<UTC>-<pid>` run roots are removed.
`sandbox_items` now lists every tmp.* older than
sandbox_min_age_minutes - with its mark where it has one and any
repository in it - and `_act_item` leaves one should it ever reach the
act. A mark in a tmp.* is a file anybody may have left (a `.lock`
holding digits), so it no longer makes one removable either.
`repository_within` also finds a BARE repository (a `HEAD` beside an
`objects/`, as `module_repos` reads one): the path itself, or one
anywhere inside it (`nested_repos(..., bare=True)`), so a cache holding
one is kept too.
Tests: the reviewer's probe (both tmp.* listed and left, the commit
still in the bare repository, and `repository_within` naming a bare
repository nested or at the top, and nothing in a notes-only one); the
two cases that held a marked or 14-day-old tmp.* to removal now hold it
to `list`. The docstring and the manual's `--include-sandboxes` row say
what `--yes` now does to a tmp.*.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n unreadable one is a note (#174 review R3)
#170 G6 made `_other_lanes` read every lane's control root the
register names, in ANY estate on the workstation, and a claim that
could not be read refused. One pathless or unreadable sidecar anywhere
therefore made the sweep exit 2 for a lane with nothing on disk (the
reviewer's probe: rc=2 at 499d2ab, rc=0 at 45fa45d) - and once #175
gates lane-end on it, every lane-end but `--inventory-only` would be
blocked by it.
Two changes, self-contained in lane-worktrees so #175 takes them when it
merges main after #174 lands:
- a lane's recorded `dir` from the register is read only where its
`.lane-state` lies under this sweep's estate root (a lane recorded in
another estate keeps its claims there); this lane's own control root,
the override and `$PROJECTS_ROOT` are read as before;
- `unreadable()` is a NOTE, not a refusal, when the sweep has no tree:
a claim only takes a tree away from this lane, so with none it can
lower nothing. With a tree it still refuses, as before.
The new test is the probe in two places: a pathless sidecar of a lane
recorded in another estate (not read: the gate exits 0, then 3 once the
lane has a tree), and one in this estate (exit 0 with the note, then 2
once the lane has a tree). #170 G6's register case put its other lane
outside the estate root; it now stands in a plain directory of a
repository inside the estate, which only the register's record reaches.
The docstring, the manual's claims paragraph and its exit contract say
so.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lane-worktrees-residue-tests (#179)
One conflict, in tests/test_lane_worktrees.py,
test_killed_suite_sandboxes_whose_owner_is_gone_are_removed: #179 swapped
the literal pid 999999 for _dead_pid() (#170 C6), and #174's R2 changed what
the test expects of a `tmp.*` (a suite's `.lock` in it is no proof; it is
listed and kept). Resolved with R2's comment and expectation and #179's
_dead_pid(): `tmp.deadbeef01` with a dead-pid pytest `.lock` is listed and
kept, while `pytest-of-$USER/pytest-*` with a dead `.lock` pid is still
removed.
Lane: openRepoTools-1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mp.* listing case
After merging #174's review fixes (R2), test_a_tmp_dir_is_listed_and_never_removed_marked_or_old
asserted the literal text "a pytest .lock (pid 999999, gone)". #179 writes the
`.lock` with _dead_pid() (a pid proved dead, never a constant a host with a
large pid_max may be running), so the row names that pid and the literal
failed: the one post-merge failure, 1 failed and 132 passed.
The test keeps the pid it wrote in dead_pid and asserts the row names it.
The sweep's behaviour and wording are unchanged.
Lane: openRepoTools-1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s, three-way against #174's final head 4ea84c8
This branch is stacked on #174 and already carried its commits up to 4ea84c8.
A plain merge of origin/main would take 500687c as the merge base and see #174's
content added on both sides. The merge is built with 4ea84c8 as the base
instead, so main's side adds nothing of #174's and the result is #179's own
changes over main.
main at merge: 5eb3d5f
Lane: openRepoTools-1
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Main is 500687c, then 5eb3d5f (#174's squash, whose tree is 4ea84c8's
exactly), then 2fb854f (#179). This branch carries 4ea84c8's own history, so
git merged on base 500687c, and #179's changes to #174's text conflicted with
#174's text as this branch carries it. No conflict touched a line #175
changed. Each file is resolved to main's text plus #175's own diff
(4ea84c8..88b2923): a three-way merge on base 4ea84c8, which merges every file
without conflict. Main's text is taken in every conflicted region:
lane-worktrees 4 regions: #179's SEAM_NO_PROCSCAN process-scan
seam (2 regions), its shell-quoted report command
(#170 E6/E7), and `self._unread(c, ...,
record=False)` in place of
`self.unreadable.setdefault(c, ...)`
lanes-edit.sh 1 region: #179's C-locale, unquoted reads in
bytecode_in_pathspec (#170 E1, E2); this file
is now main's exactly
tests/test_lane_worktrees.py 4 regions: #179's `_dead_pid()` in place of the
literal 999999 (3), and its "#170: test
honesty, portability and nits" section at the
end of the file
The files git merged on its own (docs/README-lanes.md, tests/run.sh,
tests/test_lane_helpers.sh, tests/test_lane_worktrees_report.py, and #179's
other files) are byte-identical to the same base-4ea84c8 merge.
`git diff origin/main` of this merge is byte-identical to `git diff 4ea84c888b2923` (index and @@ lines stripped, at -U0 and -U3): #175's 6 files,
+1107/-14.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
readylanding gate: runs tests-macos once before the squash
2 participants
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
This is part 2 of #170, stacked on #174, so its base is
feat/lane-worktrees-residue. #174 takes every item that can destroy or publish something, or that decides a gate. This PR takes the rest of #170: report honesty, portability, the suite wrapper, the daily start, test honesty and nits. With both landed, nothing in #170 is left open, so this PR says Closes #170.Items 3, 9, 10 and 11 were not in #170's PR 2 list. They are taken here so that "Closes" is true. (d) and B8 were begun by lane openRepoTools-3's writer on a local branch that was never pushed. They were finished and proved here.
Items, commits and tests
"Red" means the case failed with the base's (
499d2ab, #174's head)lane-worktrees,lanes-edit.sh,lane-start,AGENTS.mdandtests/run.sh, run with this branch's tests, in proof run 37507138793, jobred.%cs(git 2.21+)lane-worktreesreporttest_lane_worktrees_report.py::test_a_foreign_clones_date_needs_no_git_2_21.lane-stateonly beside checkouts the shape walk sees; an empty section said "none" beside unread recordstest_an_empty_section_says_what_could_not_be_read_never_nonetest_the_report_finds_every_kind_of_leftover_and_changes_nothing(now a checkout in a plain directory inside a repository)git grepread as "no events"_closed_lanestest_a_register_grep_that_failed_is_a_finding_never_no_events.gitignoreand both--postwritestest_bytes_that_are_not_utf8_never_abort_the_reportos.listdirof the estate roottest_an_estate_root_that_cannot_be_listed_is_a_findingtest_an_ignored_evidence_file_is_found.gitignorerepair command's path was unquotedtest_the_gitignore_repair_command_survives_a_space_in_the_pathrmtree's retry chmod-ed through a symlinktest_a_failed_removal_never_chmods_what_a_symlink_points_atgh pr list --limit 1000missed older branches' PRsprstest_a_branchs_pull_request_is_found_however_many_the_repository_hastest_the_hand_written_register_line_is_quoted_for_the_shellnote_linedid not passLANES_SESSIONtest_the_register_line_is_written_as_this_sessionLANE_WORKTREES_SEAM_NO_PROCSCAN)test_a_process_scan_that_cannot_be_made_keeps_every_treetest_merged_is_the_register_or_gh_never_ancestrytest_the_rescue_is_on_origin_at_the_moment_of_removaltest_killed_suite_sandboxes_whose_owner_is_gone_are_removed,test_a_tmp_dir_with_no_mark_of_a_suite_is_listed_until_it_is_old.pycpassed (C-quoted path)lanes-edit.shbytecode_in_pathspectest_pathspec_check.py::test_a_staged_bytecode_file_whose_name_is_not_ascii_is_refusedadd '<path>'parsed in the caller's localelanes-edit.shtest_the_add_lines_are_read_in_the_c_locale.gitignorecommand's path was unquotedlanes-edit.shtest_the_offered_gitignore_command_survives_a_space_in_the_pathstop_groupreaped a TERM-deaf leader before the KILLtests/run.shtest_run_wrapper.py::test_a_leader_that_ignores_term_is_killed_at_the_deadline--basetempwon over the wrapper'stests/run.shtest_a_callers_basetemp_is_refused[form0,form1]tests/run.shtest_what_a_suite_on_the_terminal_leaves_running_is_stoppedlane-starttest_the_daily_report_is_private_and_survives_its_panes_hanguplane-starttest_a_stamp_from_today_runs_no_second_report_and_yesterdays_is_replaced,test_a_failing_or_switched_off_report_never_fails_a_start,test_a_dry_run_start_runs_no_reportAGENTS.md, manualtest_repo_hygiene.py::test_the_suite_wrapper_takes_one_lock_and_names_it_where_agents_read_itdocs/README-lanes.mdPassing at the base by design. C1, C3, C4 and C6 are test-honesty items. Each makes a case assert what it claimed to prove, and the base's code already behaves correctly there. So 7 of the 29 selected cases pass at the base, and that is the point: they now pin that behaviour positively instead of vacuously.
gitthat checks origin at the moment ofworktree remove.Copilot round 1 on this PR
There were three threads, all real defects in this PR's own code, and each is fixed with a case. "Red" means the case failed with
lane-worktreesandlane-startat 0292005 (the head before the round) and this branch's tests, in proof run 37513097772. At e66195c in that run, thelane_worktrees,run_wrapper,pathspec_check,repo_hygieneandopenrepotools_commandcases pass (376). The one failure is the macOS-job hygiene test, which reads the trimmed proof workflow itself.Report.markdowntest_lane_worktrees_report.py::test_a_failure_that_is_no_record_leaves_an_empty_section_nonelane_daily_reportchmod 700that failed still started the reporttest_a_report_directory_that_cannot_be_made_private_runs_no_reportSweep._noteLANES_SESSIONtest_lane_worktrees.py::test_the_hand_written_register_line_is_quoted_for_the_shell(now wants the session word)Copilot round 2 on this PR
Review 5433148700 on e66195c opened no inline threads. Its two "previously missed" findings are real defects, and both are fixed. "Red" means the case failed with
lane-worktreesandtests/run.shat e66195c, in proof run 37514530247. At 7ac5cbe in that run the same selection passes (378); the one failure is the macOS-job hygiene test, which reads the trimmed proof workflow.lane-worktrees:5113: the report's untracked listing was taken whatever its exittest_lane_worktrees_report.py::test_an_untracked_listing_that_failed_is_a_finding_never_evidencetests/run.sh:175: the G14 stray scan counted its own subshell,psandawk, so every terminal run waited out the ten-second deadlinetest_run_wrapper.py::test_a_terminal_run_that_leaves_nothing_waits_for_nothingDesign notes
set -mwithfgleft the suite stopped on SIGTTIN. So the wrapper reads its process group's members before the suite starts. Afterwards, whatever is in the group that was not there before, and is not a sibling the caller started, is TERMed and then KILLed after ten seconds, before the cleanup.nohup, and undersetsidwhere there is one. Linux has it; macOS does not, and therenohupalone keeps the hangup away.AGENTS.mdstays at its 316-line cap: the one line changes in place, and a hygiene assertion holds it there.Verification
tests/run.shon the workstation was starved by other projects' suites, so CI is the record. Proofs ran on temporaryproof/openRepoTools-1/*branches with a trimmedtests.yml, each deleted once read:head(-k 'lane_worktrees or run_wrapper or pathspec_check or repo_hygiene'): 291 passed and 1 failed. The failure is the macOS-job hygiene test, which reads the trimmed proof workflow itself.red: 22 failed and 7 passed (the by-design cases above).parse-macos: bash 3.2 parses every shipped bash file.helpers: the shell suite under plainpython3 -m pytest -k lane_helpers, as CI runs it. 22 passed.helpers-base-runsh: the BASE tree's shell suite throughtests/run.sh. 4439 ok and 1 FAIL, "the row's stamp states clause (c)'s two facts for a person, in its TAIL". That failure is the base's own, not this PR's. Under run.sh's long run root, the lane row's stamp is cut before itsdir …; window …tail. The pre-rebase proof saw the same failure on this branch through run.sh. It is not touched here; it is reported to the lane.This PR's own CI is the landing record. I have not applied
ready.Closes #170
Refs #174 #176 #162
Lane: openRepoTools-1
🤖 Generated with Claude Code
Summary by Sourcery
Harden lane reporting, sweeping, daily startup, and test execution for honest failure handling, portability, privacy, and reliable cleanup.
Bug Fixes:
Enhancements:
Documentation:
Tests: