Repository navigation
Hygiene rule refuses bench-root host paths, with an allowance for quoted grep patterns; replace them in the consumer reads and the durable-intent sample - #229
Conversation
… allowance for a path inside a quoted grep pattern Part of #193 (item 22). tests/test_repo_hygiene.py's host-path rule matched /home/<user>/, /Users/<user>/, the Claude scratchpad prefix and a Windows user profile, and never the bench root, so two committed files carried 87 such lines that only a person's eye had checked. host_absolute_path_hits() now also reports every line that names the bench root at the start of a path, except inside a single-quoted search pattern of a grep (the first quoted operand after grep and its short flags, and each further -e argument), because a recorded search must keep the text it searched for. A path beside the pattern, a second quoted operand, a double-quoted pattern, and a flag that takes a value are all still findings. Red and green tests plant the path in twelve places the allowance must not reach and nine forms it must accept, replay the three recorded searches of consumer-reads-2026-10-07.md as main carries them (red) and with the root beside the pattern made a placeholder (green), and hold the home-directory rule as it was. On this commit the repository test is red on exactly the two files whose placeholders follow in the next two commits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fined once, and keep the three quoted search patterns as typed Part of #193 (item 22). Eighty-six lines of lane 2's consumer-side reads (landed in #189) named a host-absolute path under the bench root, in the "Exact commands" listing and in table and prose rows, across six roots. They now read $WB (workBenches), $BW (brett-wip), $LS (the lane-state root), $XF (xFactory), $ORT (the openRepoTools checkout), $ORTW (the openRepoTools-worktrees root) and $READS (lane 2's worktree of the planning branch), plus $PROJECTS for the one row that says what ~/projects is. All eight are defined once, in one paragraph added under the opening note. The three recorded searches whose pattern is a path (`grep -e '…/openRepoTools'` in the workBenches and brett-wip listings and `grep -rn '^path: …/openRepoTools/'` over the lane-state records) keep that pattern byte for byte, because a placeholder would change what was searched for; only the root beside each pattern is a placeholder, and the hygiene rule's quoted-pattern allowance accepts them. Nothing else changed: no finding, number, sha or result is reworded. Expanding the placeholders back and dropping the added paragraph reproduces main's file byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… as ~/projects/openRepoTools Part of #193 (item 22). The sample record's one illustrative directory was a host-absolute path under the bench root. The home-relative form keeps its meaning, the working directory of the openRepoTools lane the sample names, and passes the hygiene rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 2 hours and 54 minutes by commenting |
Reviewer's GuideThe PR strengthens hygiene enforcement for bench-root absolute paths with a narrowly scoped grep-pattern exception, then removes the newly detected host paths from the consumer-read evidence and durable-intent sample without changing the recorded search semantics or evidence. Flow diagram for bench-root path hygiene enforcementflowchart LR
A[Repository content] --> B["host_absolute_path_hits()"]
B --> C{Bench-root path found?}
C -->|No| D[No finding]
C -->|Yes| E{Single-quoted grep pattern?}
E -->|Yes| F[Allow pattern text]
E -->|No| G[Report hygiene finding]
F --> H[Report any remaining paths]
Flow diagram for replacing host paths in recorded evidenceflowchart LR
A[Consumer-read evidence with host paths] --> B[Define placeholders]
B --> C[Replace host roots with placeholders]
C --> D[Preserve grep pattern bytes]
D --> E[Expand placeholders for comparison]
E --> F[Byte-for-byte match with main]
G[Durable-intent sample] --> H[Replace bench-root path with home-relative path]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
brettheap
left a comment
There was a problem hiding this comment.
Independent adversarial review — lane openRepoTools-1 (read-only; 2026-10-17T22:06Z)
Verdict: LAND at c1a30dff628288818916191ad66c0fd1f9537f58, once CI's tests and tests-no-submodule are green. Both were pending when this was written. No finding blocks. I recommend taking findings 1 and 2 in this PR: together they are a two-line change, and I verified it below. Otherwise file them on #193.
All work was in a fresh scratch clone at that head, read-only. Pytest ran scoped to the file, with a private HOME, TMPDIR, --basetemp and PYTHONPYCACHEPREFIX. Below, <R> stands for the bench root with its trailing slash, the rule's _BENCH_ROOT, so this review spells no host path either.
What holds (checked, no finding)
-
The rule catches what main carries. I scanned every tracked blob at each commit with each revision of the rule:
rule content hits head main aa14de387 in 2 files: consumer-reads 86, ideation 1 head b32f43f(commit 1)the same 87 head 064c2ae(commit 2)1 (ideation) head head 0 main any of the above 0 Reach probes were all findings at head and all clean under main's rule:
- a path in prose mid-line, in a table cell, in inline code and in an indented code line;
- a path after
=, after:and inside( ); - a double-quoted path, a yaml value, a json value and a
file://URL.
-
The allowance holds against these, each still a finding:
- a second quoted operand of
grep, and a file after an-echain; git grep … -- '<R>…'and--file='<R>…';- a double-quoted pattern, and
grep 'x' f; cat '<R>…'; git -C <R>… grep -e '<R>…', where only the pattern is scrubbed and the line is reported.
- a second quoted operand of
-
No false positive on today's look-alikes. These are all clean:
$AGENT_PROTOCOL_ROOT/workspace.yaml,"${AGENT_PROTOCOL_ROOT:-$HOME/.agents}/workspace.yaml"and~/.agents/workspace.yaml;templates/workspace-root/and"$checkout"/workspaces/;~/,$HOME/,${HOME}/,./and../before the root;- a URL path.
At head, 115 raw lines contain the root. Every suppressed one is one of four kinds:
- a look-alike above;
- one of the three kept patterns;
- a bare root (finding 3).
-
Pytest, scoped to
tests/test_repo_hygiene.py:- head: 184 passed;
- main: 160 passed;
b32f43f: 183 passed and 1 failed. The failure istest_no_committed_file_names_a_host_absolute_path, on exactly the two files.tests/test_test_roots.pyat head: 39 passed, 1 skipped.
-
Red at base, green at head. I ran the 24 new tests outside pytest, against each rule:
- Against main's rule, 14 are red: all 12 refused rows, the scrub test and the consumer-reads pair test. The other 10 are green: the 9 accepted rows, and the home-rule test, which main satisfies trivially.
- Against head's rule, all 24 are green.
- I also ran them against 11 mutants of the rule. Nine turn at least one test red. For the two that survive, see finding 4.
-
The consumer reads: a pure rewrite, both ways.
- The PR's reverse proof reproduces:
IDENTICAL, with sha25691e1beaf…c5a3on both sides. - Independent forward proof. I rewrote main mechanically: longest root first, start of path only, with the single-quoted
grepspans left as typed. The result equals head with lines 5–17 removed, byte for byte. - The 92 placeholder uses match the PR body:
$ORT42,$WB19,$BW14,$READS7,$LS5,$ORTW2,$XF2,$PROJECTS1. - The numbers and shas in main and in head's body are the same multiset. The only difference is seven
2s, theopenRepoTools-2that$READSnow carries. - The three search lines (main 94/132/142, head 107/145/155) keep their quoted spans byte for byte: 8, 5 and 1 spans. Each still carries the root inside the pattern.
- The placeholders are defined before use. All eight are defined at lines 7–14, before any use.
$PROJECTSis defined at line 7, and its one use in the body is at line 217. None of the eight tokens appeared on main, and no other new$TOKENappears. --numstatis 99/86. Main's blob is #189's own (63810dd,4abffbf), so "this file as #189 landed it" can be recovered from git.
- The PR's reverse proof reproduces:
-
The ideation edit (
ideation/brainstorm/supervised-context-restart-durable-intent.md:31) keeps the sample's meaning. On this bench, the two spellings of the projects directory have the same device and inode, so~/projects/openRepoToolsnames the directory the sample named. The tilde form matches the estate's own records (theLANES.md--dir ~/projects/openRepoTools, andworkspace.yaml'spath:). -
No new host path in the diff. Added lines carry the root only inside the three kept patterns.
/homeand/Usersappear only as split fixtures ("/home" + "/alice/"). No/tmp/or/mnt/was added. The commit messages name/home/<user>/only as a pattern. -
#195 has no hunk overlap with this PR, and its hygiene tests pass at head. The new rule runs through
tracked_files()/tracked_roots()from #190 and #195, so the composed run applies it to the assembly and spec legs as well. -
Open PRs. None of the other 27 open PRs adds a bench-root path. I fetched each diff and grepped it; #196 has 3810 diff lines, none with the root. So landing this reddens nothing in the queue.
Findings
- Non-blocking, recommended in this PR.
grep -f '<path>'is licensed.- Where.
tests/test_repo_hygiene.py:2536-2537. - Cause. The flag run
(?:[ \t]+-[A-Za-z]+)*accepts any short-flag bundle, so the quoted argument of-fis scrubbed as if it were the pattern. That argument is the pattern FILE, a path. - Ran. These probes against head's rule:
grep -f '<R>projects/pat.txt' log,git grep -f '<R>…' HEAD,grep -rf '<R>…' .andgrep -f '<R>p.txt' -e 'x' -e 'y' log. - Saw. All four are clean.
- Why it matters. It contradicts the comment at
:2531-2535("a flag that takes a value (grep -m 1 '…') ends the run of flags"), the PR body, andconsumer-reads-2026-10-07.md:16("allows a path inside a quotedgreppattern and nowhere else"). The test row at:2985-2986passes only because its value1is unquoted. - Fix. Use
-(?![A-Za-z]*f)[A-Za-z]+for the flag run. Alternatively, derive the run from the_OPTIONS_WITH_ARGUMENTS["grep"]this file already keeps at:3149-3153. Add red rows for the four probes. - Verified. With this fix and the one in finding 2, all four probes are findings and the three allowed forms stay clean. The 24 new tests stay green. Head scans to 0, and main still scans to 87 in 2 files.
- Where.
- Non-blocking, recommended in this PR. The scrub replacement ends in a word character.
- Where.
tests/test_repo_hygiene.py:2547,QUOTED_SEARCH_PATTERNS.sub("grep", line). - Cause. A path glued to the closing quote of a scrubbed span then has
pin front of it, and the lookbehind drops it. - Ran.
grep -f ''<R>projects/pat.txt log. Saw: clean. - Fix.
.sub("grep ", line), with the same verification as in finding 1. - Related, and acceptable for a lexical rule. The allowance keys on any word
grep, not on a command position.echo grep '<R>…' > cmd.txt,my-grep '<R>…'and the prose "do not grep '…'" are all clean. One sentence in the comment would say so.
- Where.
- Non-blocking. The bare root is not caught, and the comment's reason for the trailing slash is not what does the work.
-
Where.
tests/test_repo_hygiene.py:2511-2512and:2517-2518. -
Ran. I scanned head with a variant,
(?<![\w.~$}])+ the root without its slash +(?![\w.-]). -
Saw. It flags exactly 12 lines:
lane-start:1123,lanes:65andlanes-edit.sh:6510: incident prose, "a cwd of/work…";tests/test_lane_helpers.sh:4958, 4961, 4966, 5057, 5094, 5115, 5174, 5192, 6492: a fakecwdin fixtures.
It flags none of the
workspace.yaml,workspace-root/orworkspaces/lines. The lookbehind, not the slash, keeps those out. -
The choice is untested. Mutant M6 (match the bare root) keeps all 24 new tests green.
-
Fix. Either catch the bare root and decide what the fixtures should say, or state in the comment that it is out of scope, with a green row that pins that. A regex-escaped form (
\/work…\/) is not matched either, but the home-directory rule shares that limit.
-
- Non-blocking. Two mutants survive every new test.
- M8: scrub any
-x '…'pair after the pattern. No row puts-f '<R>…'after a pattern. The rule itself is right today; nothing holds it. - M6: see finding 3.
- The other nine mutants each turn at least one test red: no allowance, any single-quoted string, double quotes allowed, no lookbehind, a lookbehind without
~/$, flags that take a number, a second operand scrubbed, the allowance anywhere aftergrep, and whole-text matching. - Fix. Add
grep -e 'x' -f '<R>…' fto the refused rows along with finding 1's rows.
- M8: scrub any
- Non-blocking, latent, no action needed. Some relative forms are false positives.
- Ran.
"$ROOT"<R>x,$(pwd)<R>x,templates/*<R>xand a<placeholder><R>xwritten with angle brackets. - Saw. All four are findings. None exists on the tree.
- Remedy. A writer who meets one can write
${ROOT}, because the lookbehind excludes}. Do not add"or)to the lookbehind: that would hidecd "<R>…"and(<R>…).
- Ran.
- Non-blocking nit, for lane 2's eye. The
$PROJECTSrow is now true by definition.- Where.
specs/004-migrate-to-triad/consumer-reads-2026-10-07.md:217reads "~/projects→$PROJECTS", and:7defines$PROJECTSas "the one~/projectsstood for there". - The fact is right; I measured it above. The row still says what it needs:
--dir ~/projects/openRepoToolsis$ORT. The wording is lane 2's call.
- Where.
Conservative, as intended, and not findings. These are all still findings at head, which fits "narrow on purpose":
rg '<R>…'andegrep '<R>…';- an
-echain broken by another flag, as ingrep -e 'a' -i -e '<R>…'; - a quote opened on a continuation line;
- an escaped quote inside a pattern.
Only the continuation-line case is documented. The others may be worth a word in the comment.
…ed pattern is still a path Part of #193 (item 22). Findings 1 and 2 of the independent review of #229 (review 5560316525). 1. grep -f '<path>', git grep -f and grep -rf got through: the run of short flags accepted any bundle, so the quoted argument of -f, a pattern FILE and so a path, was scrubbed as if it were the pattern. A bundle that carries a lowercase f is no longer a flag of the run (-F stays one), and the comment now says what the code does about a flag that takes a value. 2. The scrub replaced a pattern with "grep", which ends in a word character, so the root's lookbehind dropped a path glued onto the closing quote. It is now replaced by "grep ". Five REFUSED rows hold them: -f, git grep -f, -rf, -f ahead of an -e chain, and the glued form. Without the fix those five fail and the other 24 new tests pass; with it all 29 pass, the repository scan is clean at head, and main's content still scans to 87 hits in its two files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Folded the independent review's findings 1 and 2 (5560316525) at 636c5b6; findings 3–6 are filed as #193 item 24. Red/green: five new REFUSED rows ( |
|
Lane openRepoTools-1, 2026-10-17T22:14Z: Copilot review was requested at 636c5b6 three ways ( |
|
Lane openRepoTools-2, 2026-10-17: review of the placeholder edit to lane 2's evidence on #229 at 636c5b6. Verdict: one change requested in the added block (line 14, Below, The evidence did not move
The three kept searches (main 94/132/142 → head 107/145/155)
The placeholders
In the block the proof drops: one change requested, one suggested
If the fix keeps the block at 13 lines, the body's Optional, not blocking
CI at 636c5b6 (read once, 22:5xZ)
Lane: openRepoTools-2 |
…rds, and say single-quoted Part of #193 (item 22). Lane 2's review of #229 (comment 6242658351), applied inside the added block, which stays 13 lines (5-17). - $READS (requested): the reads ran in lane 2's worktree on its own branch 004-migrate-to-triad-reads, created from the planning branch's commit ff6f6a7, not on the planning branch itself. The definition now says so. - $ORTW (suggested): the root is the single-repository Speckit worktree root, as the file calls it; the definition no longer says more than the evidence. - The note under the list says the hygiene test allows a path inside a single-quoted grep pattern, which is what the rule accepts (a double-quoted pattern and a quoted -f operand are refused). Nothing outside lines 13, 14 and 16 changes; expanding the placeholders and dropping lines 5-17 still reproduces main's file byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
Applied lane 2's review 6242658351 at 08e4249: line 14 The PR body's stale counts are updated (five commits, 17 refused rows, 189 passed). |
|
Lane openRepoTools-2, 2026-10-17T23:0xZ: re-check of #229 at 08e4249. Verdict: APPROVE. Lane 2's review (6242658351) is applied in full, and lane 2's evidence is unchanged. Below,
Lane: openRepoTools-2 |
…R on today's main Main moved through the suite's machinery (#229's tests/test_repo_hygiene.py) and the openRepoShape pin (v1.1.0) after the last green run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…so CI tests this PR on today's main Main moved through the suite's machinery (#229's tests/test_repo_hygiene.py) and the openRepoShape pin (v1.1.0) after the last green run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Takes in #232 (tests/run.sh records its lock owner), #233 (the openRepoShape pin at v1.1.0, 190133d) and #229 item 22 (the bench-root host-path hygiene rule). The two sides touch disjoint files, and no file this branch adds names a bench-root path. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in d424b64 (#201), 61a7378 (#215, the lane-start clause (c) TAIL fix), 19c2e25 (#232, tests/run.sh's lock owner), f685866 (#233, the openRepoShape pin at v1.1.0, 190133d) and f595e44 (#229 item 22, the hygiene rule on bench-root host paths) on top of 83d2690. Both sides change lanes-edit.sh and docs/README-lanes.md, in disjoint hunks: main's are `swapped`'s row spelling (Amendment 15) and its manual paragraph, the branch's are project-legs, lane-reconcile's leg and paired-root reads and the T018 section. git merged both with no conflict. bash -n passes on lanes-edit.sh, lane-handoff, lane-start, lane-end, tests/run.sh and tests/test_lane_helpers.sh. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iene rule of #229 item 22 asks f595e44 made tests/test_repo_hygiene.py refuse a committed path that starts at the bench root, except inside a single-quoted grep pattern. docs/triad-lane-tooling-analysis.md had seven such lines (§0, §4.2, §4.5, §4.9, §4.12 and two rows of §4.15). Each now says ~/projects/..., the bench's projects directory, as f595e44 respelled the same path in ideation/; the §4.15 grep pattern keeps the spelling it searched for, under the rule's allowance. A dated note under the header says so. host_absolute_path_hits over every file this branch adds or changes now finds nothing. Docs only. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es, so the sweep, lane-end's gate, lane-handoff, lane-reconcile and the daily report can see a lane's work there, and change nothing a checkout with no project.yaml does (#246) * docs: the triad split's lane-tooling side, surface by surface (T006, T007, T009, T010, T018) docs/triad-lane-tooling-analysis.md: for every lane tool, the installer and its receipt, the hooks, the manuals, the tests, CI and the brett-wip register/inventory/handoff data, what the assembly/spec/code split of opensoft/openRepoTools changes, at file:line on c4864ac, with the plan task each change belongs to and whether it is a patch or a proposal. No shipped command changes: no lane tool reads docs/, openspec/ or specs/ at run time (the adoption plan's cross-leg follow-ups for them are comments, usage text, a URL or the object-key grammar), and every fix the triad does need changes today's behaviour, so each is a proposal with a sketch. The suite's test roots (T009/T010) are lane 1's PR #190 (c139e88); this branch's earlier draft of them was withdrawn before commit, and the document records the overlap, one gap and one measured defect in #190 for lane 1. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP (untested): T018 worktree visibility for triad legs — the legs reader, the sweep's leg and paired-tree discovery, lane-end's leg rows Checkpoint for an account swap, on the coordinator's word. NOTHING HERE HAS BEEN RUN UNDER tests/run.sh: `bash -n` passes for lanes-edit.sh and lane-end, and lane-worktrees parses as Python; one manual run of `lanes-edit.sh project-legs` against openRepoShape's assembly-root project.yaml template (pinned submodule) printed the spec and code legs, exit 0, with no workspace configured; 8 for a directory with no manifest; 64 for a relative path. DONE (T018, plan proposal 2 of docs/triad-lane-tooling-analysis.md §4.4): - lanes-edit.sh: `project_leg_paths_file` (lane-start rung 5's awk, the seven deciding lines verbatim, printing every leg) and `assembly_legs`; the read-only subcommand `project-legs <checkout>` (0 rows, 8 none, 1 manifest unreadable, 64 usage), exempt from the workspace guard; listed in the unknown-subcommand refusal and the usage header. - lane-worktrees sweep: where the lane's dir has a project.yaml naming legs, `Leg` rows for each leg and each initialized nested submodule (porcelain kind `leg`, a LEGS section, a summary suffix), counted only where the leg's HEAD's own Lane: trailer is this lane's and it holds work, or unreadable; every leg's worktree registrations enumerated; `<assembly>/worktrees` scanned as the paired root and claimed by the same HEAD-trailer rule as .claude/worktrees; legs' branches under --branches and probed under --fetch. No project.yaml: no helper call and no new code path's output. - lane-end: `leg` porcelain rows counted (retire) or listed (not counted), "leg checkout(s)" in the gate's tally, no `--yes` offered when legs are all that is counted. REMAINS, in order: (1) tests/test_triad_lane_tooling.py — assembly fixture from the pinned template bytes with legs as real repositories and bare local remotes, fake gh exiting 1; single-repository golden comparisons against c45a452's tools (CI checks out depth 1, so the old bytes need a decision: skip where c45a452 is absent, or captured goldens); the shared-awk identity check against lane-start; (2) README-lanes.md and usage text for (a); (3) commit (b) lane-handoff paired-root poll + recording the leg checkout, lane-reconcile's leg registrations; (4) (c) the daily report's legs; (5) (d) add recording the leg; (6) the shell suite once at the end. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * T018 (a): the triad guidance for the sweep and lane-end's gate — README-lanes, usage text, the dated §4.4 note; 2aa8a59's code now tested Plan task T018 (#186), slice (d2), part (a). This commit carries the guidance for the code 2aa8a59 added: a "A triad: its legs, their worktrees and the paired trees (T018)" section and a `leg` row in the porcelain block of docs/README-lanes.md; a "A TRIAD" paragraph in `lane-worktrees --help`; four lines in lane-end's CLOSE-OUT GATE header (`lane-end --help`); and a dated subsection at the end of §4.4 of docs/triad-lane-tooling-analysis.md, including the note "outputs that change regardless of shape" the coordinator asked for (the unknown-subcommand list and usage header of lanes-edit.sh, and the --help texts), for lane 1's ruling. THE INVARIANT: a checkout with no project.yaml declaring a leg other than itself is asked nothing, so a single repository's sweep rows, gate verdict, exit codes, register lines and inventory rows are unchanged. Only usage text moves for every shape, as listed in the §4.4 note. THE PROOF, run under tests/run.sh with this tree (tools as committed here and in 2aa8a59): tests/run.sh -k "t018 or repo_hygiene or test_lane_worktrees" -> 1 failed, 299 passed, 1006 deselected in 1002.77s The 299 include every test of tests/test_lane_worktrees.py and tests/test_lane_worktrees_report.py, the hygiene module, and the T018 tests: the single-repository golden comparison against c45a452's stored outputs and the assembly cases (template bytes, legs as real repositories behind bare local origins, fake gh exiting 1). The one failure was the hygiene check on host-absolute paths, tripped by the golden fixture's `<ROOT>/home/projects/`; that fixture and its test module are the next commit, WIP, with the fix. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP (untested): T018 (a) tests — tests/test_triad_lane_tooling.py and the single-repository golden captured from c45a452's tools Checkpoint for an account swap, on the coordinator's word. The module and its stored fixture, after the fix of the one failure the previous run found; the fixed form has NOT been run under tests/run.sh (the re-run, `tests/run.sh -k "t018 or repo_hygiene"`, was stopped while still waiting before the lock, exit 143, so it never held it). DONE: - The stored-fixture decision: the "before" of the single-repository invariant is tests/fixtures/t018/single-repository.golden, generated ONCE from c45a452's own tools (`git archive c45a452 | tar -x -C <dir>`, then `python3 tests/test_triad_lane_tooling.py --capture <dir>`), because CI checks out at depth 1 where c45a452 is not there to run. It covers add (dry run and act), the sweep's porcelain and table with --branches --include-scratch --include-caches, lane-reconcile, lane-end refusing and passing with the row and last log line, lane-handoff's output, top block and inventory, and the estate report, on a repository with a submodule that is no leg and on one whose project.yaml names no leg but itself. - Normalised for what no two runs share and nothing else: the bench's root and $HOME (both spellings), tree ids, operation ids, stamps, hashes, ages, byte sizes, the order of the inventory's sidecars (their file names are a checksum of the bench's path), and the sweep table's note about this account's unreadable /proc processes (a host fact). The golden test runs on Linux only, where it was captured. - The fix since the run: `$HOME` is normalised to `<HOME>` before the root, so the fixture names no `/home/<user>/` path (the hygiene failure). Captured twice from c45a452 afterwards: byte-identical. The current tools' outputs on the same scenario equal it (compared by the module's own functions, not by pytest). REMAINS: run `tests/run.sh -k "t018 or repo_hygiene"` and record the counts; then (b) lane-handoff's paired-root poll and leg-checkout record with lane-reconcile's leg registrations, (c) the report's legs, (d) add's leg record (all three drafted in the session scratchpad, not applied), and the shell suite once at the end. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP (untested): T010 docs — the §4.14 CI sketch exits 4 under #190's composed mode; three ways a composed job can pin the PR's code Plan task T010 (#186). Queued by the coordinator from the review of #190: the §4.14 sketch (code-PR CI with OPENREPOTOOLS_COMPOSED=1 against an assembly checked out at main) would exit 4, because #190 at dbc6837 refuses any code root the assembly does not pin — Roots.absent() (tests/conftest.py:283) runs _leg_at_its_pin (:259), and pytest_configure (:404-412) raises it as pytest.UsageError — while discover_assembly (:159-172) finds only an assembly that mounts the code root. Dated notes in §4.14 and §7 item 7: the composed job must check out an assembly whose code gitlink points at the PR's own commit, or a throwaway assembly built for the run that pins it, or use #190's standalone mode. Documentation only; no tool or test changes. Marked WIP (untested) because the hygiene suite has not been run over this text: the checkpoint came first. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * T018 (c): the daily report counts each leg of an assembly as a checkout of its own, with a legs section only where an assembly's leg is checked out Plan task T018 (#186), proposal 3 of docs/triad-lane-tooling-analysis.md §4.4. A leg is a submodule, its `.git` a FILE, so `Report.build` never counted one as a canonical checkout and the branches all lane work makes after the split never reached the report. Now, for every canonical checkout whose own project.yaml declares legs (read by `lanes-edit.sh project-legs`), each checked-out leg is read as a checkout of its own: its branches ("Unmerged branches…"), its root `main` ("Root main divergence") and its rescue branches ("Awaiting disposition"), each leg row ending with `the <path> leg of <assembly>`; and a section "Assembly legs with uncommitted or unpublished work" lists the legs holding dirty or untracked paths or a HEAD no origin ref holds, or that could not be read. A manifest that cannot be read is a row of "Records the report could not read". Guidance in docs/README-lanes.md's report table and `lane-worktrees --help`. THE INVARIANT: a canonical checkout with no project.yaml is asked nothing; the legs section is added only where a leg was found, so a report of single repositories is byte for byte what it was. This changes the daily report for the estate's existing triads (IRRS, IRSS, MedxEHR, openDox), which is what proposal 3 is for. THE PROOF: * For 8e2227e and 96da9af, which were WIP: `tests/run.sh -k "t018 or repo_hygiene"` -> 162 passed, 1144 deselected in 134.24s (2026-10-09T13:25Z). * For this commit: run.sh's own in-lock command under the same lock, because the wrapper's workstation-wide census had waited 30 minutes on other projects' pytest runs (#184) - on the coordinator's word, the form lane 1 approved on 2026-10-07: `flock -w 7200 /tmp/openrepotools-pytest.lock env TMPDIR=<run root>/tmp python3 -m pytest tests -q -p no:cacheprovider --basetemp=<run root>/basetemp -k "t018 or repo_hygiene or test_lane_worktrees_report"` -> 184 passed, 1124 deselected in 457.65s. That includes the single-repository golden (the report section of c45a452's stored outputs, unchanged), every test of tests/test_lane_worktrees_report.py, the hygiene module, and two new cases: an assembly whose code leg holds an unpushed branch and whose spec leg is dirty, and an estate with no assembly, which has no legs section. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * T018 (d): lane-worktrees add records the leg it made a tree from, and says of a tree made from an assembly itself that its legs are empty there Plan task T018 of #186, slice (d2), proposal 1 of docs/triad-lane-tooling-analysis.md §4.4. Where the lane's checkout is an ASSEMBLY - its project.yaml declares a leg other than itself, read only through `lanes-edit.sh project-legs`, lane-start's rung-5 reading - `lane-worktrees add` matches the checkout it makes the tree from against each leg by their common git directory (the leg's own toplevel must be the leg's path, so an uninitialized leg, which git answers as the assembly, never matches). A tree made from the leg, or from any worktree of that leg's repository (a paired <assembly>/worktrees/<feature>/code included), is recorded in the #97 inventory with THE LEG as its checkout, which every inventory reader enumerates; the dry-run and success notes name the leg, and the success note says "with <leg> as its checkout" where the checkout given was a worktree of it. A tree made from the assembly itself is made as before, and a note says a worktree does not check the legs out and names the `--checkout <assembly>/<leg>` that makes a leg's tree. A manifest that cannot be read refuses, exit 2, nothing made (Amendment 7(d)). THE INVARIANT: a lane whose checkout has no project.yaml is asked nothing (no helper call), and a manifest that declares no leg other than `.` answers 8 - so on a single repository `add`'s output, exit code, inventory row and take-back text are byte-for-byte what they were. PROOF: test_t018_single_repository_outputs_are_c45a452s_byte_for_byte compares the current tools against tests/fixtures/t018/single-repository.golden, stored from c45a452's tools, whose "add --dry-run", "add" and "add (manifest, no leg)" sections are add's; it passes unchanged. The shell suite's #163 add block (tests/test_lane_helpers.sh) runs once on the merged tree at the end of this slice. Help text that changes regardless of shape (listed in the §4.4 note for lane 1's ruling, item 2): `lane-worktrees --help`'s "A TRIAD" paragraph gains two sentences on add. Guidance: docs/README-lanes.md "In a triad (T018), a tree of a leg is made from the leg". The §4.4 note's first paragraph now names (a) through (d), with (b) a second writer's branch this one merges. Tests (tests/test_triad_lane_tooling.py, fixtures from openRepoShape's assembly-root template bytes, legs behind bare local remotes, gh exits 1): test_t018_add_records_the_leg_it_made_a_tree_from (dry run, an add from <assembly>/code, an add from a paired worktree recorded under the leg, the sweep then reading that tree through its leg) and test_t018_add_from_the_assembly_itself_says_its_legs_are_empty. COUNTS: `tests/run.sh -k "t018 or repo_hygiene or test_lane_worktrees_report"` on this tree: 186 passed, 1124 deselected in 212.76s (0:03:32), exit 0 (30f9dc0's same selection: 184 passed; the two new (d) tests make 186). run.sh's own census and lock (it waited ~48 minutes behind other runs; no flock fallback was used). The selection holds no supervised-restart test. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * T018 (b): lane-handoff sees a triad's legs — the paired trees polled, each tree recorded under its leg Plan task T018 (#186), slice (d2), part (b), first half. Two gaps docs/triad-lane-tooling-analysis.md §4.4 and §4.7 measured in the handoff's writer poll, closed SHAPE-GATED through 2aa8a59's shared reader (`lanes-edit.sh project-legs`, called as it is): (1) THE PAIRED ROOT IS POLLED. Where the lane's checkout is an assembly (its own project.yaml declares a leg other than itself), the handoff polls `<assembly>/worktrees/<feature>` and `<feature>/<each declared leg path>` as it polls `.claude/worktrees`: every directory there with a `.git` of its own (anything else under it is the assembly's working tree, where git answers too). A lane with unpushed work in a paired tree used to be absent from its WRITERS block. (2) EACH TREE IS RECORDED UNDER ITS LEG. A polled tree whose repository's common git directory is a checked-out leg's is filed with that leg (project-legs' third column, the path `add --checkout` records) as its checkout. Anything else keeps the lane's dir, as before. The first handoff used to rewrite the leg that `lane-worktrees add --checkout <assembly>/code` recorded to the assembly. A leg that is NOT checked out is passed over: git answers the assembly there, and the drafts' leg map, which lacked that guard, would have filed every tree of the assembly under a deinitialized leg (measured: with the guard deleted, the deinit case fails). AN UNREADABLE project.yaml is never "no legs" (Amendment 7(d)), and the swap still completes (R-A11-11). The paired root is not polled, the top block's WRITERS section carries a `- NOT POLLED:` line, no sidecar is filed for a tree that is not of the lane's own repository (the checkout `add` recorded is kept), and the lane stays SWAPPING. THE GATE: a checkout with no project.yaml is asked nothing. There is no helper call and no git read, and set-lane-tree gets the same argv. One whose project.yaml names no leg but itself costs one `project-legs` call, which answers 8, and nothing else. `--supervise` and `--restart-status` exit before any changed line. No usage or --help text moves. THE PROOF, under tests/run.sh, lanes-edit.sh at base (lane-reconcile is the next commit): tests/run.sh -k "t018 or supervised_restart or repo_hygiene or openrepotools_command" -rs -> 7 failed, 429 passed, 874 deselected in 3149.31s (load 60-70 on 32 cores) The 429 include tests/test_triad_lane_tooling.py, whose single-repository golden from c45a452 passes UNREGENERATED (tests/fixtures/t018/ untouched), and the new tests/test_triad_lane_tooling_handoff.py: three assembly cases built from openRepoShape's template bytes at the pin (legs as real repositories behind bare local origins, fake gh exiting 1), and a second stored golden, tests/fixtures/t018-handoff/single-repository-decoys.golden. That golden was captured twice from c45a452's own tools, byte-identical, on a single repository laid out to look like a triad: a submodule's worktree under worktrees/<feature>/code, and a tree recorded with the submodule as its checkout. Its scenario has no project.yaml, and then one naming no leg but itself. The handoff output, top block, inventory and lane-reconcile before and after all match it. The 7 failures are --restart/--supervise timing cases of tests/test_supervised_restart_regressions.py (TimeoutExpired, "observer never confirmed readiness"). A/B at load ~40, one run.sh run each: this tree: -k "<the 7>" -> 7 passed, 1303 deselected in 143.69s 96da9af (archive copy, no change): -> 7 passed, 1299 deselected in 150.59s so they are load, not this change. Against 96da9af's lane-handoff the three new assembly cases fail (measured); the base handoff turns the `add`-recorded `<assembly>/code` into `<assembly>`. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * T018 (b): lane-reconcile reads a triad's legs — each leg's worktree registrations and the paired root Plan task T018 (#186), slice (d2), part (b), second half: the third gap docs/triad-lane-tooling-analysis.md §4.4 measured. `lanes-edit.sh lane-reconcile` read only the checkout's own worktree registrations and the two lane roots, so a tree of a leg's repository that no sidecar names was invisible to it. SHAPE-GATED, through `assembly_legs` (2aa8a59's shared reader, called as it is): - EVERY CHECKED-OUT LEG'S REGISTRATIONS, read beside the checkout's own. Each worktree git registers in a leg that no sidecar names is a TREE row: `unmanaged`, or `stale-registration` where its directory is gone. The row names the leg ("git registers it in <leg>, the code leg of <assembly>/project.yaml (<repository>)"). The leg's main entry, which git spells as its git directory under `.git/modules/`, is skipped by place. A leg that is not checked out (git answers the assembly there) registers nothing of its own and is passed over. - THE PAIRED ROOT, after the lane roots: `<assembly>/worktrees/<feature>` and `<feature>/<each declared leg path>`, each holding a `.git` of its own that nothing above named (a lost registration, or a clone where a worktree belongs), is `unmanaged`. - PATHS NOBODY READ ARE NOT CLEARED: a project.yaml that cannot be read, a leg whose `git worktree list` fails, or a paired root or feature directory that cannot be listed makes the verdict `indeterminate`. The unreadable manifest is said in the words `lane-worktrees`' sweep refuses with ("the legs of the assembly <dir> could not be read"), because the sweep reads this verdict first and refuses on it. The previous run's one failure, tests/test_triad_lane_tooling.py:616, was exactly that: same exit 2, the reconciliation's wording instead of the sweep's. One fact now has one spelling, and that test passes unchanged. Nothing is adopted, deleted or overwritten, as before. THE GATE: a checkout with no project.yaml is asked nothing. One whose project.yaml names no leg but itself gets `assembly_legs`' 8 and nothing else. Every row, count and verdict on a single repository is what it was. THE PROOF, under tests/run.sh, this exact tree (lanes-edit.sh and the test module last written 2026-10-10T02:43:15Z, the run started 02:43:34Z): tests/run.sh -k "t018 or supervised_restart or lanes_index or repo_hygiene or lane_start_claude_current or openrepotools_command" -rs -> 544 passed, 768 deselected in 1318.62s (0:21:58), on a quiet box The 544 include both T018 modules. Two goldens stored from c45a452's tools pass UNREGENERATED: tests/fixtures/t018/single-repository.golden (byte identical) and tests/fixtures/t018-handoff/single-repository-decoys.golden. The latter covers lane-reconcile before and after a handoff on a repository laid out to look like a triad. Also included are the supervised-restart module, the lanes-index module (the real lane-reconcile), lane-start's module (it calls lane-reconcile), hygiene and the installer module. Two new cases are in tests/test_triad_lane_tooling_handoff.py, on openRepoShape's template bytes at the pin: the legs' registrations and the paired root are reported (a spec tree made away from every root, a paired code worktree, a clone in the paired root, a stale registration; neither leg nor its `.git/modules` entry reported), and the indeterminate verdicts (unreadable manifest, unlistable paired root). Against 96da9af's lanes-edit.sh both fail (measured). The earlier run of this half, before the wording fix: 1 failed, 446 passed, 865 deselected in 2718.97s (the :616 case above). Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP (untested): T018 (b) guidance — README-lanes says what lane-handoff and lane-reconcile read in a triad, writer 2's draft checked against 9a5bbe2 Writer 2's paragraph (briefs/t018-b-guidance.md, 2026-10-09T21:53Z) after "`lane-end`'s gate" in the T018 section, each sentence checked against the merged lane-handoff (4e39854) and lane_reconcile (9a5bbe2). Two corrections: the unreadable-manifest verdict is spelled in the sweep's words, which the sweep reads first and refuses on (exit 2); and a project.yaml naming no leg but itself is asked one project-legs call (answering 8), not nothing. Untested: committed for an account swap while the merged tree's scoped run was in flight; that run's counts follow in the next commit. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP (untested): T018 (d) — add's refusal on an unreadable manifest is tested, and spelled as the sweep and lane-reconcile spell it test_t018_add_refuses_where_the_manifest_cannot_be_read holds 13cb52d's README claim (mode-000 project.yaml: exit 2, no tree, no inventory row). The refusal reads "the legs of the assembly <dir> could not be read", the sweep's and 9a5bbe2's words, in place of "the legs of the lane's assembly <dir>". Reached only where <lane dir>/project.yaml is there and project-legs fails on it; a checkout with no project.yaml is asked nothing. Untested: committed for an account swap while the merged tree's scoped run was in flight; that run's counts follow in the next commit. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * T018 docs: a parked paired tree is not the lane's to the sweep (§7 item 11, for lane 1's ruling), and §6 says what the branch now ships docs/README-lanes.md, T018 section: a parked or resumed paired tree carries no Lane: trailer (park.sh puts the lane in the WIP subject only; resume soft-resets those commits), so the sweep and lane-end's gate count it for nobody unless the lane's inventory names it. The paragraph says what to read before lane-end in an assembly, and how to record such a tree with set-lane-tree so the gate counts it. docs/triad-lane-tooling-analysis.md: a dated §6 note on what the branch ships beyond its first commit, and §7 item 11, the choice between reading park's WIP subject and giving park's commit a trailer, plus whether resume records the tree it rebuilds. Written by the previous writer on 2026-10-11 and left uncommitted; checked here against HEAD 8cb1368, whose lane-worktrees, lane-end and lane-handoff are byte-identical to 9991d69's. Every line reference holds: lane-worktrees :2424-2431, :4473-4494 and :2124, lane-end :2122-2127, the FOREIGN reason text (lane-worktrees:3037-3039), and workBenches ffd5516 park.sh :226 and :860 and resume.sh :221-231 and :746. Three corrections in item 11: the source is review 5481327496 on #196 (a pull-request review, not an issue comment); its finding 2 was derived from code and not run, so "measured" is now "derived", the document's own word for that; and the inventory names a paired tree through lanes-edit.sh set-lane-tree, not through lane-worktrees add, which makes its trees under the lane's own root only. Docs only. No test run for this commit; the branch's one verification run follows the M1(a) and should-fix commits. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * T018 (b), pre-PR review M1 option (a): lane-handoff polls the paired root by the sweep's rule, so one lane's handoff never takes another lane's feature trees; and S1, S5, N1-N5 M1 (a). In an assembly, lane-handoff polled and recorded every .git-bearing tree under <assembly>/worktrees as the running lane's writer. The paired root is per feature, not per lane, so after one handoff another lane's paired work was this lane's inventory: lane-end refused on it, sweep --yes could rescue and remove it, and the next session was told to relaunch writers into it. Now a paired tree is polled and recorded only where this lane's inventory names it (lanes-edit.sh lane-trees, read once), its HEAD's own last Lane: trailer is this lane's on a commit origin's default branch lacks (a bash mirror of lane-worktrees' own_trailers/head_lane_trailer; a clone is never claimed by trailer, as the sweep never adopts one), or the --writers-file names it. Every other checkout there is one "- PAIRED ROOT, not this lane's: <path> — branch …" line under its own heading in the top block, outside WRITERS, not counted and not recorded. Pending lane 1's ruling on #186 between (a) and (b). S1. A paired root, feature directory or leg directory that cannot be listed or searched was skipped in silence by the handoff. It is now a "- NOT POLLED" line, a stderr note and a lane left SWAPPING, as is an inventory that cannot be read. lane-reconcile (indeterminate) and the sweep's _paired (exit 2) gain the one case they also passed over: a leg's directory under a feature that cannot be searched, and in the sweep a root or feature directory that can be listed but not searched. N1. lane-reconcile passes the feature directory to awk through ENVIRON, not -v. N3. The handoff's unreadable-manifest words are the sweep's and lane-reconcile's ("the legs of the assembly <dir> could not be read"). N4. add's hint names every leg's --checkout, not legs[-1]. N5. The handoff keeps a recorded checkout of the tree's own repository (a dependency nested in a leg) instead of re-recording the tree under the assembly. Tests. N2: the assembly cases wear conftest's NEEDS_UPSTREAM (composed mode fails rather than skips), and the no-assembly report test needs no marker. New: the two-lane M1 regression; S1's three unlistable directories (handoff and reconcile); N5; S5(b) the sweep's absent row and add's toplevel guard on a deinitialized leg; S5(c) an unreadable leg (counted, lane-end refuses), the submodule-status refusal (unshadowed by reconcile) and _paired's own refusals called directly; and the coordinator's quoted-manifest case (openRepoShape v1.0.0, #395 writes path: "{{SPEC_PATH}}"): project-legs already strips one pair of " or ' quotes, and the test holds it for both quote styles. Two existing handoff tests change for M1 (a): the no-commit paired spec tree is named by a writers file, and the paired code tree of the deinitialized-leg case carries the lane's trailer. A checkout with no project.yaml is untouched: every new path runs only where project-legs answered legs (lane-handoff, lane-reconcile, the sweep) or the manifest declares them (add). Untested in this commit: the branch's one verification run follows on the final tree, and its counts are in the commit that ends this step. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * T018 docs: README-lanes and the analysis say what M1 (a), S1 and S2 changed; the analysis's false statements about the branch are dated and corrected (S4) docs/README-lanes.md, T018 section: - M1 (a): "each lane's handoff records every paired tree" is gone. The handoff polls and records a paired tree only where the lane's inventory names it, its HEAD's own Lane: trailer is the lane's on a commit origin's default branch lacks, or the writers file names it; every other one is a "PAIRED ROOT, not this lane's" line outside WRITERS, not counted or recorded. The sweep's bullet says the handoff no longer widens its paired-root rule, and the parked-tree paragraph says a handoff does not record a parked tree for the lane. - S1: the handoff's NOT POLLED, stderr note and SWAPPING for an unlistable paired root, feature directory or leg directory, or an unreadable inventory; reconcile's indeterminate and the sweep's refusal now also cover a leg directory that cannot be searched. - S2: "no --yes acts on a leg" is now "on a leg checkout": under --branches a leg's branches are rows like the checkout's own, and --yes deletes a merged lane branch there as it does in the checkout, bundling first. - N5: a tree recorded under a checkout of its own repository (a dependency nested in a leg) keeps that checkout through a handoff. docs/triad-lane-tooling-analysis.md (S4): dated 2026-10-13 notes at §0 item 1 and §6. "This branch carries no test change of its own" was true of its first commit only; it now carries two test modules and two goldens and still changes none of the suite's roots. §6 records M1 (a) and S1, and §7 item 11 says the handoff no longer closes the parked-tree gap by itself. Verification, one run through tests/run.sh on this commit's tree, 1511f94 (4d4957d plus these docs; main f87fd5a merged at 8cb1368), 2026-10-13 19:10-19:40Z. Every module but the four below was --ignore'd, with -k "triad_lane_tooling or repo_hygiene or test_the_lane_helper_suite_passes": 181 passed, 1 failed, 1 deselected in 1818.96s. - tests/test_triad_lane_tooling.py, tests/test_triad_lane_tooling_handoff.py (both goldens unregenerated) and tests/test_repo_hygiene.py: all passed. - The shell suite, tests/test_lane_helpers_suite.py:: test_the_lane_helper_suite_passes: 4598 passed, 2 failed. Both are lane-start row-stamp assertions, "…with clause (c)'s directory and window in the tail" and "the row's stamp states clause (c)'s two facts … in its TAIL". lane-start's cut_to_line caps the stamp at 230 characters (lane-start:3502-3510). Under run.sh the suite's $HOME is ~/.local/state/openRepoTools/tmp/<UTC>-<pid>/tmp/tmp.XXXXXXXXXX/home, and the stamp then reaches 231 characters before its window value, so the tail is cut ("…/home/projects/repoA; ..."). With a /tmp-length home it is 165. This branch changes none of lane-start, tests/test_lane_helpers.sh, tests/test_lane_helpers_suite.py or tests/run.sh (git diff f87fd5a is empty for all four), so the two failures do not come from it. An earlier attempt (run1) ran nothing: zsh passed the --ignore list as one argument and pytest exited 4. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP (untested): T018: lane-reconcile reports a leg whose .git git cannot read as the sweep does - a TREE row `unreadable` that requires recovery - instead of passing over it in silence lanes-edit.sh, lane_reconcile, the legs' registrations: a declared leg whose git toplevel is not the leg itself was skipped whether or not a .git stood in it, so a leg with a .git of its own that git cannot read (a gitfile naming no repository, or a .git where git answers the assembly) said nothing, while the sweep's `leg` row for it is `unreadable` and counted (exit 3, lane-end refuses). It now takes the sweep's rule (probe_leg: toplevel not the leg, and a .git stands there) and the sweep's word: one TREE row `unreadable`, in the sweep's words ("git could not be read through it (it has a .git of its own and git could not read it: <git's answer>)"), counted under "require recovery", so lane-start's recovery read counts it too. A leg with no .git (not checked out) is passed over as before, and a path a sidecar already names is not reported twice. The verdict is not changed: the sweep counts that leg and goes on, and an `indeterminate` here is what the sweep refuses on (exit 2) before it reads the legs, which would hide its own count and break test_t018_a_leg_git_cannot_read_is_counted_and_holds_lane_end. tests/test_triad_lane_tooling_handoff.py: a triad whose spec leg's .git is a gitfile naming no repository, then an empty .git directory: one `unreadable` row naming the leg, 1 require recovery, the verdict ungraceful-stop, and the sweep still exit 3 with the leg `unreadable` and counted; readable again, and deinitialized, the leg names nothing. docs/README-lanes.md (T018 section): one sentence for the row. A checkout with no project.yaml is untouched: the block runs only where project-legs answered legs (lrc_lgrc 0). lane-handoff's own leg loop (lane-handoff:2093-2095) skips such a leg in the same way; it is not changed here. Untested: the one verification run (run3, on this tree 2f23887) waited in the shared pytest lock queue from 20:50Z and was stopped at 21:21Z, before it took the lock, for the account swap; no count was taken. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP (untested): T018: lane-handoff counts a leg git cannot read as unreadable, as the sweep and lane-reconcile do; verification not run lane-handoff, the legs' common directories (was lines 2093-2095): a declared leg whose git toplevel is not the leg itself was skipped whether or not a .git stood in it. So a leg with a .git of its own that git cannot read (a gitfile naming no repository, or a .git where git answers the assembly) said nothing, while the sweep's `leg` row for it is `unreadable` and counted and lane-reconcile's TREE row (9f23a6b) is `unreadable` under "require recovery". Worse, a tree `lane-worktrees add --checkout <leg>` made from that leg was re-recorded under the ASSEMBLY at the handoff, over the checkout `add` recorded. It now takes the sweep's rule (probe_leg: toplevel not the leg, and a .git stands there) and the sweep's words: one `- UNREADABLE LEG:` line in the top block's WRITERS section ("`<leg>` is unreadable, the <decl> leg of <dir>/project.yaml (<repo>): git could not be read through it (it has a .git of its own and git could not read it: <git's answer>)"), a note on stderr, and a count (`legs: N of the assembly's legs under <dir> unreadable`). As lane-reconcile keeps its verdict for it, the lifecycle is not held SWAPPING for the leg itself: the poll looked everywhere it looks. writer_checkout files no guessed checkout while a leg is unreadable: a tree of no readable leg's repository, not of the assembly's own and recorded under no checkout of its own repository is named and NOT recorded, so the leg `add` recorded stays, and the lane stays SWAPPING for that tree (the existing "no checkout can be vouched for" rule). A leg with no .git (not checked out) is passed over as before. A checkout with no project.yaml, or one naming no leg but itself, never runs the leg loop: single-repository output is unchanged. docs/README-lanes.md: the handoff paragraph says so. tests/test_triad_lane_tooling_handoff.py: test_t018_the_handoff_reports_a_leg_git_cannot_read_as_the_sweep_counts_it, beside the reconcile one: the spec leg's .git a gitfile naming no repository, then an empty .git directory: one UNREADABLE LEG line, the note and the count, the lane SWAPPED, the reconciliation `unreadable`, 1 require recovery and `resumable`; a tree `add` made from the spec leg keeps the spec leg as its checkout, named and not recorded, the lane SWAPPING; readable again, nothing named and SWAPPED. The deinitialized leg test now also holds that a leg with no .git is never `unreadable`. VERIFICATION NOT RUN. The one run of the T018 selection (verify.sh, run4.log, head 9f23a6b, index tree 0b691c8) waited for the shared pytest lock behind other lanes' runs from 21:59:32Z and ended exit=143 (a SIGTERM this writer did not send) at 22:26:46Z before any test ran; an account swap was called at 22:20:05Z. So this commit and 9f23a6b are both UNTESTED. Before this commit: bash -n lane-handoff is clean, the test file parses. The next run on this tree verifies both. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * T018: lane-handoff withholds only a tree that could be an unreadable leg's, names a tree git cannot read, and says what it withheld (review of bf2a220, S1, S2, N1-N4) S1. lane-handoff, writer_checkout: while a leg was unreadable, every tree not of the assembly's own repository was withheld, a worktree of an unrelated repository in the lane's own root included, so the lane stayed SWAPPING on every handoff and those records went stale. Now a tree is withheld only where it could be of that leg's repository: its common git directory cannot be read, or lies physically inside the assembly or its git directory (.git/modules/<leg>, a leg's own .git) without being the assembly's own, or the inventory records it under an unreadable leg. Any other tree keeps the lane's dir, as when every leg reads. Test: test_t018_an_unreadable_leg_withholds_no_tree_of_another_repository (a worktree of a third repository, recorded before and made after the leg broke; and one a record names under the unreadable leg). S2. lane-handoff, poll_writer: `git rev-parse --git-dir || return 0` dropped any tree whose .git git cannot read, so a damaged .git/modules/<leg> took every tree of that leg out of WRITERS and the count while the lane went SWAPPED. In an assembly (legs_state set) such a tree is now listed and counted, UNREADABLE and (NOT recorded), added to lc_inventory_missed, and named on stderr. A checkout with no project.yaml naming a leg passes over it as before, so the goldens do not move. Test: test_t018_a_tree_git_cannot_read_in_an_assembly_is_named_and_holds_the_lane_swapping (modules/spec deleted after add --checkout spec, with a paired spec tree the inventory names; and a single repository's tree, passed over). N1. The UNREADABLE LEG line's clause now depends on whether any tree was withheld ("the N tree(s) this handoff could not place are named above, each marked (NOT recorded)", or "every tree this handoff polled could still be placed, so none is withheld for it"), and a withheld tree's WRITERS line is marked (NOT recorded). The leg rows are kept as <leg> TAB <why> and written by top_block after the poll. N2. lane-handoff's UNREADABLE LEG line and note, and lanes-edit.sh's lane-reconcile TREE row for an unreadable leg, say "before relaunching a writer onto a tree of its repository": a leg is the assembly's. N3. docs/README-lanes.md says the leg count is stderr's `legs: N ...` line, never WRITERS' (N found), and describes S1, S2 and N1. N4. Test: test_t018_a_leg_tree_add_made_is_withheld_where_the_legs_git_is_an_empty_directory (the empty .git directory, where git answers the assembly in the leg, with a tree add made from it). The existing handoff and reconcile unreadable-leg tests now also assert N1 and N2. bash -n passes on lane-handoff and lanes-edit.sh; the added lines use no declare -A, ${x,,}, mapfile, readlink -f, <<< or [[. Authoring smoke check only (the one verification follows on the final head): the three new tests, the two changed ones and the handoff golden test, each called once outside pytest with LANES_* and TMUX* unset, all passed; the three new tests each failed against cf7db4a's tools, on the withheld third-repository tree, the writer count and the (NOT recorded) marker respectively. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * T018 docs: record lane 1's rulings of comment 6240800023 in the analysis's §7 (items 3, 11 and a new 12 for M1) docs/triad-lane-tooling-analysis.md §7: a dated head note naming comment 6240800023 on #186 (posted 2026-10-17T20:48Z), and each ruling at its item: - item 3: proposal 1 (the guidance) goes live now; proposals 2 and 3 go live with the T018 PR, on the condition of the read-only before/after measurement over the triads (IRRS, IRSS, MedxEHR, MedxGlass, openDox, openXdox, openWallet and the codexFactory copies), main's tools against T018's, and any verdict that flips to a refusal told to its owning lane before the landing; - item 11: (ii), park's WIP commit carries a `Lane: <lane>` trailer, and resume records the tree it rebuilds with `lanes-edit.sh set-lane-tree <lane> <path> --checkout <assembly>/<leg>`; both workBenches changes, one workBenches issue linked from the T018 PR ("workBenches issue: to be linked"); the PR does not wait for it; - item 12, new: the pre-PR review's M1, (a) as implemented in 4d4957d, with the two-lane regression test (test_t018_a_handoff_takes_no_other_lanes_paired_tree). §6's 2026-10-13 note now says the M1 ruling was made, and where. docs/README-lanes.md: the two T018 sentences that called these rulings pending now say they were made, and the parked-tree check before lane-end stands until the workBenches changes land. Docs only. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * T018: the daily report lists a leg that is also a canonical clone once (measurement finding 1) lane-worktrees, the report's _branches, _root_main and _aging iterated `canonical + [g.path for g in legs if g.checkout]`. A leg whose .git is a directory (a clone, not a submodule's gitfile) is also canonical, so it was read twice: the read-only triad measurement of 2026-10-17 (finding 1) saw 8 branch rows printed twice on this workstation, and _root_main would do the same for a leg whose main is ahead. _aging's `seen` set hid it there only where the two spellings matched. All three now read Report._checkouts(canonical): the canonical checkouts, then the checked-out legs, each once by os.path.realpath, first occurrence kept. With no leg checked out it returns canonical untouched, so a single-repository report cannot move. _leg_cell also matches a leg by real path, so the row kept (the canonical spelling) still names its leg. Test: test_t018_the_report_lists_a_leg_that_is_also_a_canonical_clone_once (an assembly whose project.yaml declares two legs that are clones: the code leg's unpushed branch, its aged rescue branch and the spec leg's main ahead of origin each appear once, each naming its leg). It needs no template. An authoring smoke check outside pytest: it passes on this tree and fails on 69f0017's lane-worktrees, on the duplicated branch row. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * T018 docs: the analysis spells bench paths ~/projects/..., as the hygiene rule of #229 item 22 asks f595e44 made tests/test_repo_hygiene.py refuse a committed path that starts at the bench root, except inside a single-quoted grep pattern. docs/triad-lane-tooling-analysis.md had seven such lines (§0, §4.2, §4.5, §4.9, §4.12 and two rows of §4.15). Each now says ~/projects/..., the bench's projects directory, as f595e44 respelled the same path in ideation/; the §4.15 grep pattern keeps the spelling it searched for, under the rule's allowance. A dated note under the header says so. host_absolute_path_hits over every file this branch adds or changes now finds nothing. Docs only. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * T018: verify 88ee9d9: 224 passed, 1 failed (test_the_lane_helper_suite_passes: two #180 row-stamp cuts, main's own); selection triad_lane_tooling or repo_hygiene or test_the_lane_helper_suite_passes Log: briefs/t018-verify-20261018T034203Z.log (lane openRepoTools-3's briefs directory), through briefs/verify.sh and tests/run.sh, with every LANES_* and TMUX* variable unset. start 2026-10-18T03:42:03Z head 88ee9d9 tree-of-index 07fa62e 1 failed, 224 passed, 1 deselected in 4045.64s (1:07:25) exit=1 end 2026-10-18T05:34:13Z The run queued behind other lanes' pytest from 03:42Z and took the lock at about 04:26Z. Every test of tests/test_triad_lane_tooling.py, tests/test_triad_lane_tooling_handoff.py and tests/test_repo_hygiene.py passed, both goldens unregenerated among them. The one failure, tests/test_lane_helpers_suite.py:: test_the_lane_helper_suite_passes: the shell suite ended "4620 passed, 2 failed, 0 skipped, 0 pending". Both FAILs are lane-start row-stamp tail cases of #180's group: - log line 100, "...with clause (c)'s directory and window in the tail, for the person reading the row" (repoA): the cell line is cut to "...home/projects/repoA; ..." (228 chars), so "; window " is gone; - log line 1967, "...having found the checkout by its real directory name, not the typed one" (repoCase): the cell line is cut to "...no launch; dir ..." (123 chars), the whole path dropped. Cause: lane-start's cut_to_line caps the state line at 230 characters, cuts there and trims back to the last space, then appends " ...". The "; dir <DIR>; window <ref>" tail comes last, and here <DIR> is about 110 characters (the run root under ~/.local/state/openRepoTools/tmp/...), so the tail is cut: in repoA after the path, in repoCase inside it, where trimming to the last space drops the path whole. Both "got" strings are exactly that cut. Not T018's: `git diff origin/main...HEAD -- lane-start tests/test_lane_helpers.sh tests/test_lane_helpers_suite.py` is empty, and T018's lanes-edit.sh hunks (usage, project_legs/assembly_legs, lane_reconcile, a comment on lane_worktree_roots, the guard exemption, the project-legs arm and the refusal list) touch neither set-row-state nor the stamp. Lane 2 measured the same group failing on main's own tree at a TMPDIR length of 86 (#186 comment 6244714538: repoA, repoA11b, repoCase and repoMix); #215 (61a7378) moved only repoA11b to a short fixed path, and repoA11b passed here. CI's tests job, with a short run root, passes this test on main (83d2690: 1498 passed, 19 skipped; f5c3bcd and 2cb14a9: success). Not re-run on main locally: the two cases run only inside the hour-long shell suite. This run covers 9f23a6b and bf2a220 (the unreadable-leg reads) and this round: the S1, S2 and N1-N4 fixes (5d2c1c8), lane 1's rulings in §7 (9a8f175), measurement finding 1 (a4e0b17), the bench-root respelling (a60cf1c), and the merges of main 2732d59, 83d2690, f595e44 (the openRepoShape pin at v1.1.0) and f5c3bcd (cf7db4a, 107958c, 69f0017, 88ee9d9). The earlier run on 107958c (t018-verify-20261018T011535Z.log) was killed at a session exit after one F (first in order: test_the_lane_helper_suite_passes) and 176 passes, with no summary. Correction to a60cf1c's message: the first of the seven respelled lines is in §1 (Scope and method), not §0. Main has since moved to 2cb14a9 and a41adb6; this head does not include them. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * T018 tests: the unreadable-leg tests ask the git under test what it says, instead of expecting git 2.43's wording (PR #246 CI) PR #246's Linux tests job (run 39113292535, job 117633782691) failed two tests: test_t018_reconcile_reports_a_leg_git_cannot_read_as_the_ sweep_counts_it and test_t018_the_handoff_reports_a_leg_git_cannot_ read_as_the_sweep_counts_it. Both wrote a spec leg's .git as `gitdir: /nonexistent/t018-spec` and expected git's quoted answer to contain that path. Git words that condition by version: - 2.43 (this bench): fatal: not a git repository: /nonexistent/t018-spec - 2.54 (alpine/git:latest, a throwaway container): fatal: not a git repository (or any parent up to mount point /) - 2.55 (CI's runner): fatal: not a git repository: (null) On CI the tools still classified the leg `unreadable`, counted it and named it: lane-handoff's stderr shows "the leg .../triad/spec is unreadable: ... git could not read it: fatal: not a git repository: (null)". The code reads the condition (git's toplevel there is not the leg, and a .git stands in it) and only quotes git's line, so the code is right and the tests were wrong. tests/test_triad_lane_tooling_handoff.py: a new helper, git_says(b, leg), returns what every reader quotes (probe_leg's _first_line, lane-reconcile's and lane-handoff's first non-empty line, or "git answers <toplevel> there"), asked of the git under test. The three gitfile checks use it. The empty-.git-directory checks keep "git answers <assembly> there", the tools' own words, which passed on CI. No tool changes; S1 and S2 are untouched. Before this commit: under a PATH shim that rewrites only the /nonexistent error line to 2.55's "(null)" form, both tests passed when called directly outside pytest (13 s and 37 s). Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Re-run CI on main fe58808: refs/pull/246/merge was stale on 14239b9 No content change: this commit's tree is ea4db0d's. Lane openRepoTools-1, the lander, pushed it on lane openRepoTools-3's word in #186 6250521287, so the landing gate's Linux run tests the merge with current main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brett Heap <service@opensoft.dev>



Part of #193 (item 22).
Host-absolute paths under the bench root were on main, and the hygiene rule did not catch them: 86 lines of
specs/004-migrate-to-triad/consumer-reads-2026-10-07.md(lane 2's consumer-side reads, landed in #189) and one illustrative line ofideation/brainstorm/supervised-context-restart-durable-intent.md. This PR widens the rule, with one narrow allowance, and replaces the paths in both files. It is five commits (the rule, the consumer reads, the ideation sample, two review rounds); the squash merge makes them one.The rule (
tests/test_repo_hygiene.py, commits 1 and 4)host_absolute_path_hits()keepsHOST_ABSOLUTE_PATHas it was and adds a second pattern for the bench root, matched only at the start of a path. A path that continues a segment, a home directory, a variable or a relative walk (~/…,$HOME/…,./…,../…,templates/<x>/…) is not the root, and neither areworkspace.yamlorworkspace-root/. The root is spelled in two pieces in the test file so the file does not flag itself.The one allowance is a path inside a single-quoted
grepsearch pattern: the first quoted operand aftergrepand its short flags (grep -rn '^path: …',grep -E '…',git grep -n -e '…'), and the argument of each further-e. A recorded search must keep the text it searched for. These all stay findings: thegit -C <path>before the grep, a second quoted operand (a file), a double-quoted pattern, a flag that takes a value (grep -m 1 '…'), a bundle that carries a lowercasef(-f,-rf:-fnames a pattern file, which is a path), a path glued onto the closing quote of a pattern, and a quote on the line after the grep.Red/green tests, all in the hygiene file:
git -C <path> grep -e '<path>', where only the pattern is licensed, and, since the independent review (commit 4),grep -f,git grep -f,grep -rf,-fahead of an-echain and the glued form;Red on main's two files: commit 1 alone is deliberately red on exactly those two files (86 hits and 1, then none after commits 2 and 3), so the rule is shown to catch what main carries. It was rebased onto #195's main (aa14de3) with no conflict; #195's rules and this one both hold. The independent review's findings 1 and 2 (5560316525) are commit 4: its five new rows fail without the change and pass with it.
The consumer reads (commits 2 and 5) — lane openRepoTools-2, please review
These are your evidence. Please check that no finding, number, sha or result changed and that the placeholders read right. The 86 lines now use placeholders, defined once in a paragraph added under the opening note (lines 5-17), with no host path in it:
$PROJECTS~/projectsstood for,/workspace/projects$WB$PROJECTS/workBenches$BW$PROJECTS/brett-wip$LS$PROJECTS/.lane-state$XF$PROJECTS/xFactory$ORT$PROJECTS/openRepoTools$ORTW$PROJECTS/openRepoTools-worktrees$READS004-migrate-to-triad-reads),$PROJECTS/.lane-worktrees/openRepoTools-2/triad-reads$PROJECTSis one more than the seven roots the task named: one table row says "~/projects→/workspace/projects" and could not be kept without it.The three search-pattern lines (94, 132, 142 on main; 107, 145, 155 here) keep their pattern byte for byte, since a placeholder in it would change what was searched for. Only the root beside each pattern is a placeholder, and the new allowance accepts them.
Byte-for-byte proof. Drop the added block, expand the placeholders, compare with main's file (
R=/workspace/projects, run from a checkout of this branch):Commit 5 applies lane 2's review (comment 6242658351) to the block: the
$READSand$ORTWwording and "single-quoted" in the note; the block is still lines 5-17, so the proof above is exact as written. Result against origin/main2732d59(the file's blob there is4abffbf, the same as at #189): identical, sha25691e1beaf08620a01df8b335b7d573bd93003ac684045bedef94958b63490c5a3on both sides, 92 placeholder uses expanded ($ORT42,$WB19,$BW14,$READS7,$LS5,$ORTW2,$XF2,$PROJECTS1). The diff is 99 insertions (86 changed lines plus the 13-line block) and 86 deletions.The ideation sample (commit 3)
directory: /workspace/projects/openRepoToolsis nowdirectory: ~/projects/openRepoTools: the working directory of the openRepoTools lane the sample names, in the home-relative form the repository's other documents use.Counts
Run scoped, with a private HOME, TMPDIR, basetemp and pycache, every
LANES_*andTMUX*variable unset, under the shared pytest lock:tests/test_repo_hygiene.pyat head08e4249: 189 passed (151 on ci: run the test suite on Linux only #227's main before Follow up #190's test roots: name a refused root's value, check the pin's commit, find paired feature worktrees, show redirected roots under -q, run the lane manual sections, and restate the installer-source test #195, 9 more from Follow up #190's test roots: name a refused root's value, check the pin's commit, find paired feature worktrees, show redirected roots under -q, run the lane manual sections, and restate the installer-source test #195, and 29 new tests here: 17 refused rows, 9 accepted rows, and 3 more);2732d59(Triad migration evidence of 2026-10-08 and 2026-10-09: test roots and CI patches, installer entry point, restore and rollback, #192's corrections, T018, Gate B status #196 and Publish rename-lane's Rule 3 handoff stamp through a same-directory temporary file and one atomic rename, so a writer killed mid-stamp leaves the handoff whole #212 on top of Follow up #190's test roots: name a refused root's value, check the pin's commit, find paired feature worktrees, show redirected roots under -q, run the lane manual sections, and restate the installer-source test #195): 189 passed;2732d59alone (86 in consumer-reads, 1 in the ideation file);tests/test_test_roots.py: 39 passed, 1 skipped (the pinned openRepoShape submodule is not checked out in the worktree), last run at636c5b6; commit 5 changes only the consumer-reads block;testsjob is the record.🤖 Generated with Claude Code