Skip to content

Every register write goes through its lane's own checkout <W>-lanes/<lane>/, where lane-start and lane-end read the lane's row; a LANES_REPO naming W refuses; locks name their holder's container; refresh-workspace fast-forwards W (Amendment 23) - #16

Draft
brettheap wants to merge 63 commits into
mainfrom
feat/198-a23-slice-1a-routing

Conversation

@brettheap

@brettheap brettheap commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

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

Re-homed from opensoft/openRepoTools#282

This is slice 1a of act 2 (Amendment 23), re-homed to the code leg, opensoft/openRepoTools-code, by cherry-pick and not by merge, in the stack order (Brett Heap's Q28 answer; the dates are on opensoft/openRepoTools#198, comment 6722244541: 1a and 1b open as draft PRs here by 2026-11-10). The old PR, opensoft/openRepoTools#282, stays as the archived original until Brett Heap decides (T019); nothing more is pushed to its branch. Below the tests section is the original body, with four edits: its "Stacked on" and "What to review" paragraphs are rewritten for this base; three blocks that the new tests table or the archived original supersede are cut (the first run's before and after table, the broad-selection table, the per-survivor list of proposed cases), each with a line saying so; and bare issue numbers are qualified with the old repository's name, because on the code leg an unqualified reference means this repository's own numbers.

Old (opensoft/openRepoTools) Code leg (this PR)
Branch feat/198-a23-slice-1a-routing feat/198-a23-slice-1a-routing
Slice head before the merge 6ca31581c4410e736ce78da373a12388e94da939 4d6bc1493b839d374c0d8578e101c2ea4fc5a82f
Base f18b818 0cefa1fd1fd52663e692d992f09f60a421fc646d
Head of this PR f0d363a02c44030360195d4b0c4fdce262086132 (the merge of code main e393958, after 903d751's merge of 577110e)

Method.

  • The base map is old f18b818 to code 0cefa1f: same subject, author and author date; filter-repo only rewrote the short shas in its message.
  • Each of the slice's 61 commits (42 plain, 19 merges) was rebuilt with git commit-tree. Its old tree is cut to the code leg's 26 owned top-level paths (the leg: code entries of the cutover plan, specs/004-migrate-to-triad/adoption-plan-cutover-2026-10-26.yaml on opensoft/openRepoTools-spec at aed1852), and its parents are mapped. Author, author date and committer date are kept; the committer is Brett Heap. The message is kept byte for byte, plus a (cherry picked from commit <old sha>) line placed as git cherry-pick -x places it.
  • Spec-leg hunks are dropped: none at the net level for 1a (the net diff has no path outside the 26; its spec patch is empty). Nine commits are empty here and are kept, so the receipt stays one to one: seven touched only docs/, which the cut removes, and two were empty already (a CI re-run and a verify record). Two signed merges lose their signature, because a rebuilt commit cannot be re-signed.
  • A check of every old and new pair (tree, parents, author, committer, message, headers), over all six slices (227 commits), found no mismatch.

The merge of the code leg's main.

  • 903d751 merges main 577110e with --no-ff. main is 22 commits past 0cefa1f: the code leg's front door (AGENTS.md, CLAUDE.md, README.md, the LF rule, the ignore list and the licence), the tests workflow's standalone checkout context, the assembly root's two length caps, the shell suite's lane-start row-stamp cases, six of the old main's eight commits since f18b818 (the ubuntu-24.04 pin among them), and code PRs 3 to 7, T017's five lanes-edit.sh fixes.
  • f0d363a merges code main e393958 (Pin openRepoShape at v1.1.1, 5017830 on its main: four of the eight mounted files moved; no test expectation moved #15, openRepoShape v1.1.1's pin) with --no-ff, so that the landed tree is one that was tested. The merge was clean, and its tree (14d48cf) equals git merge-tree's. It brought five files, +12 -8: upstream/openRepoShape (190133d to 5017830), contracts/openreposhape-pin.yaml, .github/workflows/release.yml, tests/test_release_workflow.py and tests/test_install_payload_source.py. At f0d363a, with upstream/openRepoShape checked out at 5017830, the 12 modules that read the pin, the submodule or release.yml gave 1479 passed, 0 failed, 31 skipped (standalone-layout, composed-acceptance and built-wheel skips only), release_workflow's 499 among them. CI runs the whole suite on this head.
  • It is clean: no hunk was resolved by hand. The tree is de12f63c7864efad23c9737e0852f280a5c00549, the one git merge-tree gave before the merge was made.
  • Why: the stack takes the code leg's main, with T017's five lanes-edit.sh fixes. Slices 1b, 2a, 2b, 3 and 4 take it through this one.
  • The diff against the code leg's main is this slice alone: 19 files, +3782 -130 (the original body below says 16 files, which was before its fix rounds).

Range-diff summary (git range-diff f18b818..6ca3158 0cefa1f..4d6bc14 --creation-factor=60):

  • 32 pairs show as changed. Each differs by the added cherry-pick line and, in some, a dropped docs/ hunk, and by nothing else.
  • 10 pairs are unmatched. Nine are the empty commits above, each against its empty twin. The tenth is old 5fcdf6a against new 17d8431: its patch id, cut to the 26 paths, equals the new commit's (106 added lines against 14, which is why range-diff does not pair them at that factor).
  • The 19 merges are not shown by this git (2.43); the pair-by-pair tree check above covers them. Nothing is unexplained.

Reading the original body below. Its shas are old shas; the receipt maps each to its new one. The ones it names most: d0010a2 is d7b8c6a, 51efdb3 is 9e1d14e, 59b39df is 871b27a, fda7808 is 4e02fa6, the last merge 9d26089 is 281be58, fix round 1's head 874e020 is a4e4462, ecb96ed is 7eb779a, fix round 2's eead86d is d22c8a2 and fix round 3's 15c8e3f is cb05cfc. "Pushed" there means pushed to the old branch. Ten-digit numbers in parentheses are comment ids in the old repository. The branch's old head, 6ca3158 (here 4d6bc14), is one commit past the original body's last section: it marks the routing exec for shellcheck's SC2093 (lanes-edit.sh, 3 lines added; no behaviour changes).

Receipt: the 61 rebuilt commits, old sha to new sha (subjects cut to 56 characters)
Old sha New sha Subject
c9ffdce04 adf63c82f WIP: checkpoint before account swap; done: opensoft/openRepoTools#194 proving ...
9a29bbba1 c7a3bd142 commit_push publishes the local commits a write with ...
352c1b6f0 43cfe910c test_commit_push_published_edit_194: the refusal case ...
836e1e128 89d5f6279 opensoft/openRepoTools#214 review: only 'commit' publishes the commits a ...
fab2cbc57 2a368d581 test_commit_push_published_edit_194: the no-op case ...
f34c993e3 5efc5932d opensoft/openRepoTools#214 adversarial check: 'commit' reads the queue on the ...
5b68127fd 002b27f29 Exit-code tables: say the opensoft/openRepoTools#194 refusal pushes none of ...
9f8aea86f 76e939e9d opensoft/openRepoTools#214 reviews (openRepoShape-2 6242165339 ...
ee33a3309 9227b6259 Say what the publish fence checks: register and handoff ...
656b19145 1ebc1eafe opensoft/openRepoTools#194: a publish the pull empties exits 0 and says so ...
41433eb3f 75702bcbd Merge openRepoTools-4's opensoft/openRepoTools#214 head 656b1914 as the ...
c45a452a1 fc0f99941 docs: the triad split's lane-tooling side, surface by ...
2aa8a59f1 138b8b53f WIP (untested): T018 worktree visibility for triad legs ...
5fcdf6a17 17d8431cf T018 (a): the triad guidance for the sweep and ...
8e2227eaa 8d28a3f50 WIP (untested): T018 (a) tests — ...
96da9afc7 09b8d4a6b WIP (untested): T010 docs — the §4.14 CI sketch exits 4 ...
30f9dc089 cc4d24809 T018 (c): the daily report counts each leg of an ...
13cb52d05 3f3f4f31f T018 (d): lane-worktrees add records the leg it made a ...
4e39854b8 b00fbc409 T018 (b): lane-handoff sees a triad's legs — the paired ...
9a5bbe24d 834f53651 T018 (b): lane-reconcile reads a triad's legs — each ...
e15336104 8ac1e5897 Merge feat/triad-lane-tooling-handoff (T018 (b), writer ...
b71e547d0 c5d9adbe3 WIP (untested): T018 (b) guidance — README-lanes says ...
9991d6978 72ee697a7 WIP (untested): T018 (d) — add's refusal on an ...
8cb13680f 85f28c794 Merge main f87fd5a into feat/triad-lane-tooling (T018) ...
941330ad7 0eb57d191 T018 docs: a parked paired tree is not the lane's to ...
4d4957db6 b59e36ef3 T018 (b), pre-PR review M1 option (a): lane-handoff ...
6658396e4 97469c90f T018 docs: README-lanes and the analysis say what M1 ...
7757eaab2 a13253085 Merge main aaa7bc8 into feat/triad-lane-tooling (T018) ...
9f23a6b09 86cb75be7 WIP (untested): T018: lane-reconcile reports a leg ...
bf2a220ae d0da540d1 WIP (untested): T018: lane-handoff counts a leg git ...
cf7db4a77 e502a29bd Merge main 2732d59 into feat/triad-lane-tooling (T018)
5d2c1c8dc 8e3a4f9b4 T018: lane-handoff withholds only a tree that could be ...
9a8f17549 9968c5119 T018 docs: record lane 1's rulings of comment ...
107958c7a 84f330be5 Merge main 83d2690 into feat/triad-lane-tooling (T018)
69f0017d8 4e017566f Merge main f595e44 into feat/triad-lane-tooling (T018)
a4e0b17fa 3b82a844c T018: the daily report lists a leg that is also a ...
a60cf1cc4 3bc4f268f T018 docs: the analysis spells bench paths ...
88ee9d9c3 06c5d1331 Merge main f5c3bcd into feat/triad-lane-tooling (T018)
3d0642c00 097f0b23c T018: verify 88ee9d9: 224 passed, 1 failed ...
ea4db0d1c a700ef307 T018 tests: the unreadable-leg tests ask the git under ...
ad0c031a6 d598c647e Merge openRepoTools-1's opensoft/openRepoTools#246 head ea4db0d1 as the ...
ef16bbe0a 53e69898c Merge openRepoTools main 586dea25 (opensoft/openRepoTools#247, opensoft/openRepoTools#237) into act ...
d0010a299 d7b8c6a64 WIP: every register write goes through its lane's own ...
465df5343 0aa7c8d4a Merge openRepoTools main fe588082 (opensoft/openRepoTools#204, opensoft/openRepoTools#206, opensoft/openRepoTools#208 ...
51efdb39d 9e1d14ec3 lane-start and lane-end read a lane's row in its own ...
59b39df22 871b27a2b The shell suite's opensoft/openRepoTools#67 cases (from main's opensoft/openRepoTools#221, issue ...
129e20158 2c35ea174 Merge branch 'main' into ...
eff9107b7 d1b65b13d Merge branch 'main' into ...
fcf7f4857 a280513ee Merge opensoft/openRepoTools#214's final head eff9107b (its refresh over main ...
0d4ade894 af5929852 Merge opensoft/openRepoTools#214's squash 9b94a597 into act 2's slice 1a with ...
fda7808d6 4e02fa631 tests/test_lanes_index.py: its Estate names its old W ...
6d22d0b53 027c345fb Merge openRepoTools main b707bd70 into act 2's slice 1a ...
5dd0df07b 6467cce66 Re-run CI on main fe58808: refs/pull/246/merge was ...
eb9eb7495 3f326e0d2 Merge opensoft/openRepoTools#246's final head 5dd0df07 into act 2's slice 1a ...
9d26089ad 281be5854 Merge opensoft/openRepoTools#246's squash 55aae16 into act 2's slice 1a with ...
18f515516 0beb37dd2 Merge openRepoTools main f18b818 (opensoft/openRepoTools#252, opensoft/openRepoTools#264, opensoft/openRepoTools#266 ...
874e02041 a4e446231 Fix round 1 of opensoft/openRepoTools#282 (lane 3's review): routing is ...
ecb96eda1 7eb779a9e Fix round 1's surviving mutants are killed: 12 cases ...
eead86da5 d22c8a29e Fix round 2 of opensoft/openRepoTools#282 (lane 3's re-review): without the ...
15c8e3f81 cb05cfc2a Fix round 3 of opensoft/openRepoTools#282 (lane 3's re-check of eead86d) ...
6ca31581c 4d6bc1493 SC2093: mark 1a's routing exec as deliberate, as lane ...

Tests on the code leg

Head 903d751. Each module ran in its own pytest process, one at a time about 25 s apart, with EMAIL set, every inherited LANES_* and TMUX* unset, GH_TOKEN=bogus, a stub gh first on PATH, GIT_SSH_COMMAND=false and, except for the one module run again below, GIT_CONFIG_GLOBAL=/dev/null. The list is 46 modules: the five a23 modules, the lane tools' own, the workflow, hygiene, package, install and release modules this slice's files touch, and the five modules the code leg's main gained for T017's fixes (test_dir_with_space_295.py, test_subfield_misreads_301.py, test_unsearchable_control_root_291.py, test_pathless_tree_record_302.py, test_named_read_unreadable_290.py).

Module Result
test_a23_lane_checkout.py 27 passed
test_a23_refresh_workspace.py 13 passed
test_a23_write_routing.py 41 passed
test_a23_lock.py 24 passed
test_a23_lane_start_read.py 10 passed
test_dir_with_space_295.py 4 passed
test_subfield_misreads_301.py 118 passed
test_unsearchable_control_root_291.py 10 passed
test_pathless_tree_record_302.py 18 passed
test_named_read_unreadable_290.py 22 passed
test_tests_workflow.py 3 passed, 27 skipped
test_repo_hygiene.py 220 passed, 17 skipped
test_guard_launch_mode.py 13 passed
test_test_roots.py 39 passed, 1 skipped
test_package.py 30 passed, 5 skipped
test_package_stamp.py 140 passed
test_release_workflow.py 5 passed, 494 skipped
test_register_hardening_120_122.py 46 passed (second run; see the first note)
test_command_builtin_errexit_292.py 14 passed
test_register_scan_hardening_138.py 29 passed
test_wrapper_hardening_138.py 22 passed, 2 skipped
test_pathspec_check.py 3 passed
test_skill_shell_blocks.py 6 passed
test_install_by_rename.py 2 passed
test_wip_init_command.py 63 skipped
test_handoff_skill_98_99_100.py 29 passed
test_handoff_skill_commit_block.py 24 passed
test_commit_push_published_edit_194.py 25 passed
test_handoff_skill_72_80.py 53 passed
test_handoff_skill_80_85.py 41 passed
test_install_hook_gate.py 17 passed, 10 skipped
test_install_skill_and_hook.py 73 passed
test_lane_start_row_stamp.py 3 passed
test_triad_lane_tooling_handoff.py 1 passed, 21 skipped
test_triad_lane_tooling.py 5 passed, 22 skipped
test_deferred_97_findings_157.py 9 passed
test_lane_start_hardening_138.py 20 passed
test_lane_worktrees_report.py 22 passed
test_precision_90a_96.py 25 passed, 1 skipped
test_lane_start_claude_current.py 34 passed
test_install_payload_source.py 93 passed, 16 skipped
test_lane_worktrees.py 118 passed
test_rename_publication_atomic_158.py 20 passed
test_lanes_index.py 98 passed
test_openrepotools_command.py 96 passed, 1 skipped
test_lifecycle_fail_open_113_114_115.py 44 passed

Total: 1709 passed, 0 failed, 680 skipped, over 46 modules.

  • The one first-run failure was the runner's, not the merge's. test_register_hardening_120_122.py ran first with GIT_CONFIG_GLOBAL=/dev/null in its environment, and its numstat case writes the global config it needs into its own temporary $HOME (core.attributesFile), so with that variable set the case saw exit 1 where it expects 5. Run again without the variable it passed all 46.
  • The skips are the code leg's own. The cases that read another root's files skip with their reasons: README.md, CLAUDE.md and AGENTS.md are the assembly root's, docs/README-lanes.md is the spec root's, the pinned openRepoShape is not checked out, and OPENREPOTOOLS_TEST_WHEEL is unset. That is why test_repo_hygiene.py reads 220 passed and 17 skipped here, where the original's 237 passed. test_wrapper_hardening_138.py skips 2 cases because SIGINT is ignored at entry in a background run.
  • test_lanes_index.py passes all 98. The original's host failed 3 cases there (a session uuid in the scratch path, and a timing case); the scratch path here holds none. test_a23_lock.py passed 24 in 116 s, test_dead_holder_is_taken_over inside its 45 s bound (load average about 28 on 32 cores).
  • Not run: the shell suite (test_lane_helpers_suite.py) and test_supervised_restart_regressions.py whole, by the lane's rule. The original ran both (see its tables). The merge leaves the second file as it was, but brings main's changes to the shell suite's two files, so CI's tests job is where it runs here.

Amendment 23, act 2, slice 1a: the routing, S1, S2, S5 and the locks.

This is a DRAFT, opened for review. It lands by merge commit, on Brett Heap's standing word for act 2's six re-homed slices (opensoft/openRepoTools#198, comment 6723968257): "land each once its CI is green and lane 3's review passes, by merge commit".

Stacked on

The code leg's main (577110e), merged in as 903d751. Nothing else is open: both parents of the original landed, as its body says (old shas, in the old repository):

None of the nine merges needed a hand-resolved hunk.

What to review: the diff against the code leg's main is this slice alone, 19 files. Its four first commits are d0010a2 (the slice), 51efdb3 (the lane-start and lane-end read), 59b39df (the shell suite's opensoft/openRepoTools#67 cases) and fda7808 (the lanes-index fixture), old shas; here they are d7b8c6a, 9e1d14e, 871b27a and 4e02fa6. Fix rounds 1 to 3, below, come after the last merge of the original, 9d26089 (here 281be58).

Decisions

Each was made before the code and recorded in the slice's progress file.

  1. Routing comes after the existing gates. It runs after the workstation guard and the "registry not found" gate, and before the verb arms.

    • The dispatcher works out the lane a write is attributed to (write_lane_of, the lane its commit subject carries).
    • Under the default resolution, the file runs again by exec, keeping the same pid. It exports LANES_REPO, LANES_DIR and LANES_FILE at <W>-lanes/<lane>/, plus two internal markers, LANES__CHECKOUT_LANE and LANES__PID.
    • In the re-run, LANES__PID == $$ marks the process as the re-run itself. Any other value was inherited from a routed parent, so it is dropped together with the three paths.
  2. An explicit LANES_REPO is honoured in place, with no re-exec: it is the test seam and an operator's override.

  3. LANES_DIR and LANES_FILE locate files inside the checkout and never choose it.

    • A write whose LANES_DIR or LANES_FILE lies outside the checkout it goes through refuses (2).
    • The check runs before any clone, so a refused write clones nothing.
  4. No lane, no write. These refuse (2) and never fall back to W:

    • commit, log, claim and release with no LANES_LANE;
    • an add-row whose row has no backticked lane;
    • an append-line that names no lane.

    A writer missing its own lane argument keeps its arm's usage refusal, word for word.

  5. Some verbs are not routed.

    • Reads.
    • The dry runs: archive-rows and migrate-state-cells without --yes, and request-handoff --dry-run.
    • publish-handoff. Its callers still pass W's file until slices 2b and 3, and lane-start still commits that file in W. It takes only the sidecar lock.
  6. S1 creation.

    • The checkout is git clone --reference W --single-branch --branch main of W's origin, made through <W>-lanes/.<lane>.partial and then mv, under a creation lock <W>-lanes/.<lane>.lock.
    • A leftover .partial is moved aside, never deleted.
    • The target is checked absent again before the mv, so a clone never nests.
    • An empty directory counts as absent.
    • A W that already borrows objects is not referenced, so alternates stay one level deep.
    • W's repo-local user.name and user.email are copied.
    • The clone's budget is ten minutes, or LANES_GIT_TIMEOUT where that is longer.
  7. S1 verification. Anything at the path that is not a clone of the register is refused (1). That covers:

    • a file;
    • a plain directory;
    • another repository;
    • a .git file (a linked worktree);
    • another origin;
    • alternates that name anything but W's objects, or a store that is gone.

    Nothing there is deleted, moved or repaired. The refusal names the mv … .aside-<UTC> a person runs, and, for the alternates cases, the cherry -v to look at first.

  8. After a rename:

    • The old name's checkout (from lanes/aliases.tsv) is moved to the new name once.
    • Where both exist, the new one is written and the old one is reported.
    • The checkout is named for the row's own spelling.
  9. The locks.

    • The pid file is one line, <pid> <container> <host>.
    • Same container and host: the pid test alone, so a live holder is never aged out.
    • Another container, or no pid at all: the ten-minute age of the directory, which the holder refreshes in git_net before every network call.
    • A takeover is a claim, not a delete. The lock is renamed to a name only this process uses and read again there. It is removed only if still stale, and otherwise put back.
    • A lock is released only while it is still this process's.
    • A lock taken before an exec is adopted only with the marker. This is built for slice 1b's replay.
  10. The workstation's sidecar lock is <W>-lanes/.lane-state.lock (ort186 Q3).

    • set-lane-state, set-lane-tree, set-restart-intent and publish-handoff take it and no other lock.
    • The lifecycle follow-up inside a routed write takes it inside the checkout lock: the order is always checkout lock, then sidecar lock.
    • No tool takes a lock in W any more.
  11. refresh-workspace is the only act that changes W: git fetch origin main and git merge --ff-only.

    • It reports and skips (2) on: a live legacy lock in W; a branch other than main; an operation in progress; unmerged or staged entries; a failed fetch; ahead; diverged; or git's own refusal. Nothing in W is ever reset, stashed, committed or aborted.
    • It names each stray under handoffs/ and lanes/ by the lane whose row names it, and where in that lane's checkout it belongs (handoff_in_checkout).
  12. lane-start and lane-end read the lane's row where its writes go. Without this, slice 1a alone broke them:

    • a new lane's first start added its row through the lane's checkout;
    • W's working tree never showed the row;
    • the second start then refused at lane-start:1591, "origin/main ALREADY has a row … and this checkout does not";
    • and lane-end found "no row for lane".

    So, under the default resolution, both ask lanes-edit.sh lane-checkout <lane> --create and read that checkout's lanes/LANES.md. A --dry-run creates nothing and reads W where no checkout exists. A helper that predates the verb writes W itself, so W is read. Only the read moves: LANES_REPO, and the handoff lane-start commits, stay W's until slices 2a-3.

    One line in each also changed: a caller's LANES_REPO without LANES_FILE now reads that checkout's register, which is where the helper writes.

  13. Existing fixtures that asserted writes in W now name their old W by the explicit seam (ort186 Q9).

What changed

lanes-edit.sh:

  • New functions:
    • routing: write_lane_of, lane_checkout_enter, lane_checkout_inside, path_real, path_under;
    • S1: lane_checkout_path, lane_ws_objects, lane_checkout_verify, lane_checkout_refuse, lane_checkout_old, lane_checkout_ensure;
    • stray paths: ws_relative, handoff_in_checkout;
    • S5: refresh_workspace, refresh_workspace_strays;
    • locks: lock_ident, mutex_read, mutex_local, mutex_stale, mutex_steal, mutex_try, mutex_release, lock_adopt;
    • the sidecar lock: sidecar_lock_dir, sidecar_lock_use, sidecar_try, sidecar_release;
    • cleanup_for_exec.
  • Changed:
    • the load-time block: the markers, W always resolved, and LANES_DIR/LANES_FILE defaulting from LANES_REPO;
    • the dispatcher's routing call;
    • release_lock and lock_try;
    • git_net's heartbeat;
    • lane_state_follow, which now takes the sidecar lock;
    • the four sidecar arms;
    • cleanup.
  • New verbs:
    • lane-checkout <lane> [--create]: 0 and the path; 8 when absent; 1 when not a clone; 2 when the clone fails; 4 for the creation lock; 64 usage;
    • refresh-workspace: 0 fast-forwarded or current; 2 skipped; 1 when there is no W; 64 usage.
  • Docs in the file: the verb list, the usage lines, the exit-code table, and the environment doc (LANES_REPO, LANES_WORKSPACE_ROOT, the markers, the locks).

lane-start and lane-end: lane_register_follow, the same block in both, and the one-line derivation (decision 12).

Tests:

  • New:
    • tests/test_a23_lane_checkout.py
    • tests/test_a23_write_routing.py
    • tests/test_a23_lock.py
    • tests/test_a23_refresh_workspace.py
    • tests/test_a23_lane_start_read.py
  • Changed (decision 13):
    • tests/test_lane_helpers.sh:
      • the head: the unset list, the LANES_*/TMUX* sweep, gc/maintenance on origin and W, the gitfile view, and the seam;
      • the 9(a) refusals, which run with the seam unset;
      • 24 LANES_WORKSPACE_ROOT=<clone> cases, which become LANES_REPO/LANES_FILE;
      • the pid's first field;
      • set-lane-tree's wait, which now holds the sidecar lock;
      • the foot guard, which gains <W>-lanes checks;
    • tests/test_lane_helpers_suite.py: the unset list and the prefix sweep;
    • tests/test_lane_start_claude_current.py: the Sandbox, also used by the supervised-restart, deferred-97, rename-158 and worktrees-report modules;
    • tests/test_commit_push_published_edit_194.py: the Box;
    • tests/test_lane_start_row_stamp.py: the Sandbox;
    • tests/test_triad_lane_tooling.py: the Bench, and normalise mapping W's view back to <HOME>/projects/wip;
    • tests/test_supervised_restart_regressions.py: the two writer-mutex cases now hold the sidecar lock.
    • tests/test_lanes_index.py: the Estate.
  • Every new fixture sets gc.auto=0 and maintenance.auto=false on origin and W.

Tests (original runs, in the old repository)

Each module ran on its own with /usr/bin/python3 -m pytest -q -p no:cacheprovider -o tmp_path_retention_policy=failed, with GH_TOKEN=bogus, a stub gh first on PATH, and every inherited LANES_* and TMUX* unset.

  • Before: the stacked base ad0c031/ef16bbe. The last rows were run on an export of ef16bbe merged with main fe58808.
  • After: this branch at 9d26089.

The before and after table of this first run (36 rows: the stacked base against the final tree 8e6a5cab, and the tree before the restack) is in the archived original. The five new a23 modules passed 22, 21, 15, 10 and 4 cases there; the fix rounds below give their later counts, and the table under "Tests on the code leg" above gives every module's count at this head. Its one failure note: test_lanes_index.py had 2 failed and 80 passed on that tree, and the same two fail at main b707bd70: test_offline_no_read_the_inventory_acts_are_byte_identical_against_an_unreachable_index and test_a_poisoned_worktrees_table_changes_no_inventory_acts_answer, because the module's normaliser rewrites part of the session uuid in that runner's basetemp path unequally between the roots it compares.

Mutants. 61 one-line mutants of lanes-edit.sh were each run against the new tests meant to kill them: 60 killed, 1 survived.

  • The survivor is the check_lane_name before canon_lane in lane_checkout_enter. It is redundant with the check on the canonical name after it, and stays as the guard before canon_lane reads with the raw name.
  • One mutant (set-restart-intent's sidecar lock) is killed by the edited supervised-restart case.
  • Control: the same tests against the unmutated helper: 63 passed.
  • Four more mutants of lane-start/lane-end were run by hand, and all four were killed:
    • the follow call removed in each command;
    • the dry-run condition removed;
    • the old derivation restored.

Not run: tests/run.sh and the full pytest suite. The shell suite did run, through tests/test_lane_helpers_suite.py on the final tree.

Leaves every writer working

  • Routing is opt-in (fix round 1, MUST 2). Without LANES_ROUTE_WRITES=1 every write goes through LANES_REPO, W by default, and every lock is that checkout's, as on main. With it, and the default resolution, every write goes through the lane's checkout and is committed and pushed there (S3-S4, the fetch before the edit and the read-back, are slice 1b's). An explicit LANES_REPO still writes in place; a per-profile clone is warned about; a lane window whose LANES_WORKSPACE_ROOT names its own lane checkout is refused under the opt-in, naming that variable (MUST 3).
  • lane-start and lane-end read the row where it is written (decision 12). A second start and an end of a new lane work under the default resolution, which tests/test_a23_lane_start_read.py shows.
  • The handoff writers are unchanged. publish-handoff still writes the file its caller names, and lane-start still commits the handoff in W, until slices 2a-3. W's handoff paths still resolve.
  • Reads still read W, and workspace-root still answers W.
  • W itself moves only by refresh-workspace, and nothing calls it yet. A person runs it. lane-start calls it from slice 2b. Until then, readers of W's working tree read what W has.
  • A mixed workstation. An older helper still running beside this one takes W's mutex for the sidecar acts, and this one takes the sidecar lock, so the two do not serialise those acts with each other during rollout. The fence in each act stays the correctness check (ort186 Q3).

Not changed

Main is merged (fix round 1, below): 18f5155 merges main f18b818, past 677acc0.

Known limits, all for slice 1b:

  • The index lag is closed by fix round 1's MUST 1: W's origin/main is fetched after a routed push, before the nudge.

  • A lane checkout is only as fresh as its own last write, until slice 1b's fetch before the edit.

  • S3-S4 (the fetch before the edit, the replay and the read-back) and S6's sweeps: later slices. Lock adoption across exec is built for slice 1b's replay and used by nothing yet.

  • lane-handoff, lane, lane-rename and lane-worktrees. The handoff writes move in slices 2a-3.

  • docs/README-lanes.md.

  • handoff_in_checkout is wired only into refresh-workspace's stray report. Wiring it into set-restart-intent --handoff waits for slice 2b, because lane-start's restart checks compare the bound path with the W path it still publishes.

  • Two things found and left alone:

    • rename-lane's lane_state_rename runs under the checkout lock only, so it is no longer serialised with the sidecar writers. Taking the sidecar lock there belongs with slice 1b's rename replay.
    • Caller-set LANES_LOG_DIR, LANES_ALIASES_TSV and LANES_REPOS_TSV pass through unchecked. S2 names only LANES_DIR/LANES_FILE.
  • Reads and the sidecar verbs still log_sync (fetch) in W. A fetch is one of S5's two acts in W.

  • Every decision lane-start takes from the row's cell still comes from the published row (register-row), whatever the lane checkout's copy says.

Fix round 1 (lane openRepoTools-3's review at 9d26089)

Head 874e020. Two commits on 9d26089, both plain, nothing forced:

  • 18f5155 merges main f18b818 (past 677acc0). There was one conflict, in lane_state_follow, resolved as planned: this branch's sidecar_release, with main's $LSS_WHY note. No lsf_own is left, and main's lanes-edit.sh changes since 55aae16 add no lock call.
  • 874e020 is the fix round itself.

How routing is opted into (MUST 2). Export LANES_ROUTE_WRITES=1. Any other value, or none, keeps main's behaviour:

  • every write goes through LANES_REPO, which is W by default;
  • the sidecar acts and lane_state_follow take the checkout's own lock;
  • lane-start and lane-end read W.

The opt-in stays off until slice 1b's S3 lands. No install or release should make it the default before then.

Still on by default:

MUST

  1. The guard on a fresh session. Under the opt-in, once a push from a checkout that is not W has landed, cleanup fetches W's origin/main before the index nudge. The fetch goes through git_net, with gc and maintenance off. So the prompt guard's offline read, the index and every LANES_NO_FETCH=1 read see the write.
    • A failed fetch is a note and never fails the write.
    • lane-start needs nothing of its own, because each of its routed writes is followed.
    • This also closes the index lag this body listed as a known limit.
    • Tests:
      • test_a_routed_write_brings_W_origin_main_along: W's origin/main equals origin after the write; an offline register-row read of W shows it; W's HEAD and tree are unchanged.
      • test_a_follow_fetch_that_fails_is_said_and_fails_nothing.
  2. Sequencing. Routing is behind LANES_ROUTE_WRITES=1, as described above.
    • The two tests this slice had moved onto the sidecar lock are main's again: the shell suite's set-lane-tree wait, and test_supervised_restart_regressions.py, whose file is now main's byte for byte.
    • Tests:
      • test_without_the_opt_in_a_write_goes_through_W_as_before;
      • test_without_the_opt_in_a_sidecar_act_takes_the_checkouts_lock;
      • test_without_the_opt_in_a_new_lane_starts_through_W.
  3. Eagle's lane windows. A W whose parent directory is a *-lanes one refuses (2). So does a W that LANES_WORKSPACE_ROOT names when LANES_REPO names the same checkout.
    • The refusal says to unset LANES_WORKSPACE_ROOT, and to keep LANES_REPO to go on writing through that checkout.
    • Where it applies: lane-checkout and refresh-workspace always, and every write and sidecar act under the opt-in.
    • The check runs before anything derives <W>-lanes, so nothing is cloned or locked inside a lane checkout.
    • The env doc no longer calls LANES_WORKSPACE_ROOT "the test seam; nothing else sets it".
    • Migration for 1b: unset LANES_WORKSPACE_ROOT in lane windows.
    • Test: test_a_W_that_is_a_lane_checkout_refuses_naming_LANES_WORKSPACE_ROOT, with three cases: both variables set, the root alone, and the same checkout outside -lanes.
  4. The heartbeat. It is touch -c in git_net and at the creation lock, so a lock that vanished is never made again as a file.
    • Test: test_a_lock_that_vanished_under_its_holder_is_not_made_again. The lock is removed between the write's pull and its push. No file is left behind, and the next write passes in under 30 s.
  5. test_register_scan_hardening_138.py. Its Register writes through wip by LANES_REPO, with W a clone of its own (LANES_WORKSPACE_ROOT). It runs 28 passed: the 27 it had, and the new case.
    • The default-resolution case for the capture: test_a_routed_sweep_refuses_over_a_peer_edit_in_W_and_captures_nothing (SHOULD 4).

SHOULD

  1. append-line attribution. write_lane_of reads the published register (rows_named_ci) first and W's working tree second. A routed arm whose checkout predates the row falls back to the lane it was routed by.
    • Test: test_append_line_attribution_reads_the_published_register, in two cases: a fresh checkout, and a stale one.
  2. A checkout that cannot be made. When it fails (2) or its creation lock is held (4), lane-start and lane-end say so and read W, as the 8 branch does.
    • Test: test_a_checkout_that_cannot_be_made_is_said_and_the_start_goes_on, in which the clone fails and the start exits 0.
  3. S5 compares the repository. It checks the checkout's root (--show-toplevel) and its git common directory, not the path as typed.
    • Test: test_S5_compares_the_repository_not_the_spelling, with LANES_REPO at W's lanes/.
  4. A hand edit in W. A routed write refuses (2) while W's lanes/LANES.md has an uncommitted edit. The refusal names the edit and the place in the lane's checkout where it belongs.
    • Tests: test_a_hand_edit_in_W_refuses_a_routed_write (commit), and the hardening_138 case above (a sweep).
  5. The daily report. lane-worktrees lists <W>-lanes/<lane> under "Register checkouts (Amendment 23 S1)", with what each holds that origin lacks, and never under FOREIGN.
    • The section appears only where there are such checkouts, as the legs section does, so T018's golden is unchanged.
    • Test: test_a_lanes_register_checkout_is_reported_and_never_foreign.
  6. The stale note. Under the opt-in, lane-start's carry note says no write will push a commit made in W.
  7. Tests. Every new case uses a W that is a clone of its own, so W can be seen to lag. MUST 1's test fails without the follow-up fetch.
    • The gitfile views in the shell suite, T018's bench and the lanes-index Estate stay. Those fixtures now run in the default mode, where nothing is routed.

NIT

Fixed:

  1. -- ends request-handoff's options in the router too, as it does in the arm (test_request_handoff_why_after_the_separator_is_no_dry_run).
  2. The wording now says the sidecar lock is taken alone, after the write has released the checkout lock, and that no act holds both. This covers the env doc, sidecar_try, lane_state_follow and cleanup.
  3. mutex_release compares the container and the host as well as the pid (test_release_leaves_a_lock_with_its_pid_in_another_container).
  4. A heartbeat touches the creation lock (touch -c) every minute while the clone runs. Its output goes to /dev/null, and it stops with the clone, on exec, or on exit.
  5. lane-end now says "Nothing was written."
  6. LANES_NO_GIT=1 makes no clone: an absent checkout refuses (2) and names lane-checkout --create (test_no_git_makes_no_clone).
  7. An explicit LANES_REPO still needs a lane under the opt-in (test_an_explicit_LANES_REPO_still_needs_a_lane).

Left:
4. The three-way steal race. A mkdir mutex has no atomic compare-and-delete. Both fixes considered, a steal mutex or flock, either move the race or change what the lock is made of. The window needs a dead holder and three takers inside one rename. It is a known limit for slice 1b.
6. The clone happens before the arm validates its arguments. The checkout made is the lane's own, and its next write uses it. Copying every arm's argument checks into the router would make a second copy that can drift.
10. The other creator's lock names (.create.lock, .creation.lock, .<lane>.create.lock in Eagle's brett-wip-lanes/). That launcher's protocol is not in this repository. Aligning the two names belongs to its owner, with slice 4.

SonarCloud

The four copies of the test Box are now one module, tests/a23_box.py (RegisterBox). Each module keeps only its own rows and helpers. The new cases share the module's helpers and copy no setup. SonarCloud passed at 874e020.

Runs on 874e020

Each module ran in its own pytest process, with EMAIL set and LANES_*/TMUX* unset.

Module Result
test_register_scan_hardening_138.py 28 passed
test_a23_write_routing.py 34 passed
test_a23_lock.py 19 passed
test_a23_lane_start_read.py 6 passed
test_a23_lane_checkout.py 22 passed (the shared box, before the other modules gained their cases; this file did not change after)
test_a23_refresh_workspace.py 10 passed (the same)
test_lane_worktrees_report.py the new case: 1 passed (the whole module is in the broad selection below)
test_repo_hygiene.py 237 passed
test_test_roots.py 39 passed, 1 skipped
test_guard_launch_mode.py 13 passed

CI at 874e020: tests, tests-no-submodule, guard-launch-mode, package and SonarCloud all passed.

The broad selection at 874e020 ran the same modules as the table under "Tests on the code leg" above, which supersedes its table; the old table is in the archived original. Every module passed there, except test_lanes_index.py with 3 failed and 79 passed on that host: two cases compare paths and fail there because the scratch path holds a session uuid that their normaliser rewrites (<HEX>-<TREE>), and fail the same way at the base b707bd70; the third, test_a_hung_indexer_delays_no_writer_beyond_the_fork, is a timing case that missed its stub under that host's load. CI's tests passed all three at 874e020.

Mutants:

  • Mutants, new set (fix round 1): 33 one-line mutants (one of them two lines, SHOULD 3's literal compare) of lanes-edit.sh, lane-start, lane-end, lane-worktrees and the hardening_138 fixture, each run against the fix round's new tests: 20 killed, 13 survived. Twelve survivors are killed by 12 proposed cases (listed in the archived original), each run failing with its mutant and passing without it. One is equivalent: S5's --show-toplevel clause alone (the common-dir clause already covers it). Control: the 84 killer cases against the unmutated files passed; the hardening_138 seam under LANES_ROUTE_WRITES=1 passed 28.
  • Mutants, old set (M01-M61) re-checked at 874e020: 60 killed, 1 survived (M31, as before), 0 retired, 0 BAD. Seven were re-pointed: M03, M07, M08, M11, M41 and M43 at the line that now carries the behaviour, and M50 at test_a23_lock.py's new set-restart-intent case, because the supervised-restart case is main's again. M31 (the check_lane_name before canon_lane) is not redundant after all: a hand-made aliases.tsv row mapping a name no log can carry onto a real lane gets past it. The proposed case kills it.
  • The 12 survivors (N03b, N05, N09, N11, N13, N14, N15, N16, N21, N22, N23, N25) and M31 each got a proposed case, which ecb96ed adds (see "Since then"); the per-survivor list is in the archived original.

Since then:

  • The broad selection is complete: every module passes, the supervised-restart file and the shell suite included. The only exception is test_lanes_index.py's three cases on this host, explained above, which CI passes.
  • ecb96ed adds the 12 proposed cases, as a plain fast-forward on 874e020. CI is green there: tests, tests-no-submodule, guard-launch-mode, package and SonarCloud. Lane openRepoTools-3's re-check (6291134805) is ADEQUATE AFTER FIXES.
  • Fix round 2 is pushed as eead86d. It answers lane openRepoTools-3's N1, N2, SHOULD 5's *-lanes parent and N5 (6290167859), and the re-check's R3-1 and R3-2. See its section below.

Fix round 2 (lane openRepoTools-3's re-review at 874e020 and re-check at ecb96ed)

Head eead86d, pushed. It is one plain commit on ecb96ed, and nothing was forced: git ls-remote gave ecb96ed before the push and eead86d after it. Main is not merged in, because no conflict or test needed it. Line numbers below are at eead86d.

Every proposed fix is taken. N5 is taken in a narrowed form, and the reason is given under N5.

SHOULD

  • N1: default mode keeps main's ten-minute rule.
    • What changed: mutex_stale (lanes-edit.sh:1306-1307). Without LANES_ROUTE_WRITES=1, a live holder in this container whose lock is over ten minutes old is stale, as main's lock_try has it.
    • Why a working writer is safe: git_net's touch -c heartbeat refreshes the lock before every network call, in both modes.
    • Under the opt-in, unchanged: a live holder is never aged out (S2).
    • Every taker follows the one rule: W's mutex, which the sidecar acts also take without the opt-in, and the creation lock, whose heartbeat refreshes it every minute.
    • The refresh note: refresh-workspace's note on a stale lock in W no longer calls it "an older helper's leftover" (:15464), because a quiet live holder can now be stale too.
    • Docs: the env doc and the lock comments say this (:659-664, :1257-1262, :1584-1586).
    • Test: tests/test_a23_lock.py:515 test_without_the_opt_in_a_live_holder_is_aged_out_at_ten_minutes, with 2 parameters, pid-container-host and pid-only.
      • W's mutex is held by a live sleep, aged 11 minutes.
      • The write takes the lock over in under 30 s and lands. The holder itself is left running.
      • At ecb96ed both parameters fail with exit 4, after the 60 s wait.
  • N2: one dirty W now stops only commit.
    • What changed: ws_pending_refuse refuses only commit (:15308), the verb that publishes a hand edit.
    • Every other routed write says the same thing as a WARNING and goes on. The edit stays in W, untouched, and nothing captures it.
    • Test: tests/test_a23_write_routing.py:448 test_a_hand_edit_in_W_warns_every_other_routed_write_and_stops_none. After a hand edit to repoA23-2's row in W, repoA23-1's set-row-state lands, and origin does not carry the edit. At ecb96ed it exits 2.
    • One existing test changed: fix round 1's sweep case in tests/test_register_scan_hardening_138.py pinned the refusal that N2 removes. It is now test_a_routed_sweep_over_a_peer_edit_in_W_warns_and_captures_nothing (:620). The sweep warns, retires the row through the sweeper's own checkout, and leaves W's HEAD and the edit as they were.
  • SHOULD 5: a lane window's W.
    • What changed: where the parent of real(W) is a *-lanes directory, lane-worktrees takes that parent as the lanes home (lane-worktrees:5528-5534). It requires a non-empty name before -lanes, as lanes_home_check does.
    • Test: tests/test_lane_worktrees_report.py:725 test_a_W_that_is_a_lane_checkout_lists_its_siblings_as_register_checkouts. With W at brett-wip-lanes/repoA-1, both lane checkouts are listed as register checkouts and neither as FOREIGN. At ecb96ed both are listed FOREIGN.
  • R3-1: the two lane-end cases check that the end goes on.
    • Both assert in-flight read from lanes/log/ (tests/test_a23_lane_start_read.py:225, :281). Lane-end prints that line only after the follow-up.
    • Lane 3's mutant n21b is now killed by both cases, each failing on that assertion.

NIT

  • N3: the check takes no lock in W.
    • What changed: the git status in that check runs with GIT_OPTIONAL_LOCKS=0 (:15305).
    • Measured first: a plain git status --porcelain -- <file> rewrote a stat-dirty index with a new inode. With the variable set, it did not.
    • Test: tests/test_a23_write_routing.py:472 test_the_hand_edit_check_takes_no_lock_in_W. W's register is made stat-dirty, and a routed write leaves W's .git/index with the same inode and mtime. At ecb96ed the inode changed.
  • N4: the message only.
    • What changed: where both variables name the path: that workspace.yaml gives, lanes_home_check now calls that checkout W itself, the readers' copy no write goes through (S5). It says to unset LANES_REPO, and it no longer calls W "a lane checkout" (:14947-14953).
    • Unchanged: exit 2. A lane checkout is still called one.
    • Test: tests/test_a23_write_routing.py:489 test_both_variables_naming_W_itself_refuse_and_say_it_is_W.
  • N5: taken, narrowed.
    • What changed: lane_row_gate (:15332-15342), called just before lane_checkout_ensure (:15398). Where a lane has no checkout yet and the verb's arm needs exactly one row of that lane, a lane with no row on origin refuses (2) and nothing is cloned.
    • The gated verbs: set-row-state, replace-in-row, append-session-id, and rename-lane, which goes through its old name.
    • A miss is read again after one log_sync of W. A read that could not be made, or a fetch that did not run (failed, LANES_NO_FETCH=1 or LANES_NO_GIT=1), refuses nothing, and the arm decides as before.
    • Why it is narrowed:
      • log, claim, release, commit, append-line, request-handoff and the sweeps take a lane with no row. An archived lane keeps its log (Amendment 16(e)), so a gate there would refuse writes that their arm accepts.
      • add-row makes the row.
      • A rename's new name never routes, and the second read covers a new name that W has not fetched yet.
    • Tests:
      • tests/test_a23_write_routing.py:505 test_a_write_that_needs_a_row_clones_nothing_for_a_lane_with_none. At ecb96ed it cloned the checkout, then refused with "expected exactly 1 row".
      • :520 test_a_row_W_has_not_fetched_is_no_reason_to_refuse, with 2 parameters, fetched and no-fetch. A row a peer published, which W has not fetched, is written in both. These pin the head's behaviour, and they pass at ecb96ed too.
  • R3-2: both margins taken now, at no cost at the head.
    • Sandbox.start takes a timeout (tests/test_lane_start_row_stamp.py:206). Its default stays 120, so every other caller is unchanged.
    • N22's second start and N23's end get 300 s (tests/test_a23_lane_start_read.py:164, :263, :276).
    • N15's clone polls up to 30 s for the first beat instead of sleeping a set 4 s (tests/test_a23_lane_checkout.py:419). At the head the case ends at the first beat, and the heartbeat mutant still fails it, after 30 s.

Runs

Each module ran in its own pytest process, with every LANES_* and TMUX* unset, EMAIL set and a stub gh.

Module Before (ecb96ed) After (eead86d)
test_a23_write_routing.py 35 passed 41 passed
test_a23_lock.py 22 passed 24 passed
test_register_scan_hardening_138.py 29 passed 29 passed
test_lane_worktrees_report.py 21 passed 22 passed
test_a23_lane_checkout.py 26 passed 26 passed
test_a23_refresh_workspace.py 12 passed 12 passed
test_a23_lane_start_read.py 10 passed 10 passed
test_lane_start_row_stamp.py — 3 passed
test_lane_worktrees.py — 118 passed
test_wip_init_command.py — 63 skipped
test_repo_hygiene.py — 237 passed
test_guard_launch_mode.py — 13 passed
test_test_roots.py — 39 passed, 1 skipped
test_supervised_restart_regressions.py (main's lock holders) — 173 passed

The new tests against ecb96ed's scripts. Only the test files were laid over a clone of ecb96ed. Of the 14 cases, 8 failed and 6 passed.

  • Failed, each for its intended reason: N1 ×2, N2, N3, N4, N5's no-row case, the sweep case and SHOULD 5.
  • Passed, as designed: N5's two guard cases, N15's poll, the two R3-1 lane-end cases and N22's start. They pin the head's behaviour, and the mutants below kill them.

Not run: the shell suite, because no shell-suite file changed and CI's tests job runs it; and macOS bash 3.2. A plain VAR='' func call and $(...) are the only new shell forms.

Mutants

Each mutant is one line, built on a clone of eead86d, never in the worktree, and run against the tests meant to kill it. The control is every killer node id against the unmutated head: 17 passed. After the run all four targets' SHA256 match the head again.

The new set: 12 of 12 killed.

Id What the mutant undoes Killed by
F01 N1: the opt-in gate made return 1 the N1 test, both parameters
F02 N1, the other way: the gate removed, so the opt-in ages out a live holder test_live_holder_is_not_aged_out
F03 N1: the age test removed test_without_the_opt_in_a_sidecar_act_takes_the_checkouts_lock
F04 N2: every write refuses again the N2 test and the sweep case
F05 N2, the other way: commit no longer refuses test_a_hand_edit_in_W_refuses_a_routed_write
F06 N3: GIT_OPTIONAL_LOCKS=0 removed the N3 test
F07 N4: the yaml comparison removed the N4 test
F08 N5: the gate's call removed the no-row test
F09 N5: the second read removed [fetched]
F10 N5: the fetch check removed [no-fetch]
F11 N5: log_sync removed the no-row test
F12 SHOULD 5: the parent rule removed the report test

Fix round 1's mutants re-run against their changed killers: 6 of 6 killed.

  • n21b, lane 3's mutant, is killed by both R3-1 cases.
  • N21, N22, N23, N15 and N27 are still killed.

Fix round 3 (lane openRepoTools-3's re-check at eead86d)

Head 15c8e3f813ed26ee123dfff8b91bd77f51433952, pushed. It is one plain commit on eead86d. Nothing was forced: git ls-remote gave eead86d before the push and 15c8e3f after it. Main is not merged in. Line numbers below are at 15c8e3f.

This answers lane 3's re-check of eead86d (6301653436), whose one stopping item was R4-2. The proposed fix is taken as written.

R4-2 (NIT): refresh-workspace refused the real W while the opt-in was off

  • The defect: lanes_home_check's "W itself" branch (both LANES_WORKSPACE_ROOT and LANES_REPO naming the path: workspace.yaml gives) is S5's refusal, and S5 holds only under LANES_ROUTE_WRITES=1. The branch ran unconditionally from lane-checkout and refresh-workspace. Without the opt-in, refresh-workspace exited 2 naming an opt-in that was off, for a verb that writes no register. Its own contract reads exit 2 as "skipped, nothing changed", so a caller carried on with a stale W.
  • The change: lanes-edit.sh:14955, the first line of the branch, before the die: [ "$LANES_ROUTE_ON" = 1 ] || return 0. A four-line comment above it (:14949-14952) says why. Under the opt-in the refusal and its words are unchanged.

The four checks

  1. Callers of lanes_home_check (:1399, :15355, :19513, :19537 at this head; no other script calls it):
    • :1399 (sidecar_lock_use) comes after :1398, [ "$LANES_ROUTE_ON" = 1 ] || return 0, so it runs only under the opt-in. No change.
    • :15355 (lane_checkout_enter) has one caller, inside if [ "$LANES_ROUTE_ON" = 1 ] && [ -z "$LANES_ROUTED" ]. Only under the opt-in. No change.
    • :19513 (lane-checkout) and :19537 (refresh-workspace) are unconditional, and are the two that change. Without the opt-in, both variables naming W now go on: refresh-workspace fast-forwards W, and lane-checkout prints or makes <W>-lanes/<lane>, each as it does with LANES_REPO unset. That is right, because without the opt-in a write goes through W as on main, and W is W itself, not a lane checkout.
    • lane-start and lane-end reach the two verbs only through lane_register_follow, which returns first unless LANES_ROUTE_WRITES=1.
    • A *-lanes parent, and both variables naming a lane checkout that is not W, are still refused without the opt-in. The guard sits only inside the W-itself branch.
  2. LANES_ROUTE_ON is set first: :1137 sets it to 0 and :1138 to 1 for the opt-in or a re-run process. That is its only assignment, at load, before every caller.
  3. The N4 test (tests/test_a23_write_routing.py:489): its Box subclasses RegisterBox, which exports LANES_ROUTE_WRITES=1 by default (tests/a23_box.py:56, :92-93). It already runs under the opt-in, still refuses, and needed no change.
  4. Other tests with both variables at W and lane-checkout or refresh-workspace: none. Every other test that sets LANES_WORKSPACE_ROOT names a lane checkout, two different checkouts, or an empty LANES_REPO.

Tests

  • tests/test_a23_refresh_workspace.py:189 test_without_the_opt_in_both_variables_naming_W_still_refresh_it. A peer pushes, then refresh-workspace runs with LANES_ROUTE_WRITES="" and both variables at W. It expects exit 0, no S5 refusal, "fast-forwarded W", W's HEAD equal to origin's main, and no <W>-lanes.
  • tests/test_a23_lane_checkout.py:358 test_without_the_opt_in_both_variables_naming_W_are_no_refusal. lane-checkout <lane> with the same set-up answers 8 (none there yet) with nothing made, where it refused with 2.
  • At eead86d's script (the tests alone), both fail for the intended reason: exit 2 with S5's "both name ... which is W itself" refusal (assert 2 == 0 and assert 2 == 8). With the guard, both pass.

Runs

One module per pytest process, every inherited LANES_* and TMUX* unset, EMAIL set, a stub gh, GH_TOKEN=bogus. Counts at eead86d, then at 15c8e3f:

Module eead86d 15c8e3f
test_a23_refresh_workspace.py 12 passed 13 passed
test_a23_write_routing.py 41 passed 41 passed
test_a23_lane_checkout.py 26 passed 27 passed
test_a23_lane_start_read.py 10 passed 10 passed
test_a23_lock.py 24 passed 24 passed
test_lane_worktrees_report.py 22 passed 22 passed
test_repo_hygiene.py 237 passed (fr2's run) 237 passed
test_guard_launch_mode.py 13 passed (fr2's run) 13 passed
test_test_roots.py 39 passed, 1 skipped (fr2's run) 39 passed, 1 skipped
test_supervised_restart_regressions.py 173 passed (fr2's run) 1 failed, 172 passed

The one failure is test_restart_observer_confirms_late_uuid_prepared_by_real_child[zombie]. It is a subprocess.TimeoutExpired: lane-handoff --supervise did not finish inside the test's 30 s while five lanes shared the host (the module took 2229 s, against fr2's 1805 s). It does not touch lanes-edit.sh's home check. Run alone at 15c8e3f, both parameters of that test pass (2 passed in 54 s). The full module was not run a second time.

The mutant: NOT RUN

At the usage checkpoint the coordinator asked for no new test run. The mutant was set up in a clone: the head checked out and the guard line deleted. It was then put back and the file compared byte for byte with the head's lanes-edit.sh (cmp: identical). So no mutant result is claimed. The two new cases are the ones it would run, and both already fail at eead86d's script, which differs from the head only by that line and its four-line comment.

Not in this commit

R4-1 (lane_row_gate, for the other verbs that name a lane) is a SHOULD for slice 1b and goes to slice 1b's writer. lane_row_gate is untouched. Under the opt-in, refresh-workspace with both variables at W still exits 2 with the S5 message, as lane 3 asked for the opt-in-off case only.

Refs opensoft/openRepoTools#198

🤖 Generated with Claude Code

brettheap and others added 30 commits October 9, 2026 00:28
…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>
(cherry picked from commit c45a452a14a1c2e46d2008652547e9529e6cc154)
…ader, 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>
(cherry picked from commit 2aa8a59f16891e329b0696f4f6e31e17374348dd)
…ME-lanes, usage text, the dated §4.4 note; 2aa8a59's code now tested

Plan task T018 (opensoft/openRepoTools#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>
(cherry picked from commit 5fcdf6a17961af1d1382d27e39dbf0aef5368fe5)
… 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>
(cherry picked from commit 8e2227eaa4fdc0afc3a6404927e381821103264a)
…composed mode; three ways a composed job can pin the PR's code

Plan task T010 (opensoft/openRepoTools#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>
(cherry picked from commit 96da9afc7a22dece1b2a121850955e09a17cd445)
…ut of its own, with a legs section only where an assembly's leg is checked out

Plan task T018 (opensoft/openRepoTools#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>
(cherry picked from commit 30f9dc089cd9706c6b6507cfb4058aa66e895fbe)
… says of a tree made from an assembly itself that its legs are empty there

Plan task T018 of opensoft/openRepoTools#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>
(cherry picked from commit 13cb52d057a64690296172a7ec5e92a099914da3)
… each tree recorded under its leg

Plan task T018 (opensoft/openRepoTools#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>
(cherry picked from commit 4e39854b8e7f3570ab07931e6b8fa911b8b033c1)
…egistrations and the paired root

Plan task T018 (opensoft/openRepoTools#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>
(cherry picked from commit 9a5bbe24d02f26a942c07b21748f5a2375faca21)
…off and lane-reconcile see a triad's legs

Plan task T018 of opensoft/openRepoTools#186, slice (d2), part (b), as two
commits on 96da9af: 4e39854 (lane-handoff polls the paired root and records
each tree under its leg) and 9a5bbe2 (lane-reconcile reads each checked-out
leg's worktree registrations and the paired root). A plain merge into (a), (c)
and (d); a trial merge-tree was clean, and the merged tree's counts are in the
commit that adds (b)'s guidance to docs/README-lanes.md.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e15336104723facad36dde55a77eb1e92f64542d)
…ff 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>
(cherry picked from commit b71e547d0f16693e9500fb5065cbfe0293a66c94)
… 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>
(cherry picked from commit 9991d697849b5aee070ffe1ea3c451ef0314ef05)
…ch on the tree the PR will be judged on

origin/main at f87fd5a carries #188's openRepoShape pin (7f84ca4), #189, #192
and #190's explicit test roots. A trial merge was clean; no file is changed
on both sides. The merged tree's counts are in the commits that follow.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 8cb13680f9a7dd441bcdca8c49bf4dcd7d616bc0)
… not yet run; left: run it, the commit_push fix, both RECOVERY texts, the reset warning, manual lines, PR

done: tests/test_commit_push_published_edit_194.py, the proving test for
opensoft/openRepoTools#194, written first and not yet run (the run queued
behind another lane's full suite on the shared pytest lock and was stopped
for the swap). Six cases: the issue's own sequence end to end (exit 3, the
peer commits its file, the printed re-run must publish); a clean checkout
with nothing ahead stays a no-op with no network call; a checkout behind
origin stays a no-op; a publish past a peer's dirty file keeps the
dirty-elsewhere guard; the conflict RECOVERY warns that reset --hard drops a
peer's unpushed commit; refuse_dirty_checkout names the act to re-run.

left: run the test on unmodified lanes-edit.sh to see it fail; the fix in
commit_push (when nothing is staged and origin/<branch>..HEAD is non-empty,
go on to the push loop); the two RECOVERY texts (commit_push's
dirty-elsewhere exit 3, refuse_dirty_checkout); the reset --hard warning in
the conflict RECOVERY; the matching manual lines in docs/README-lanes.md
(about :95-97 and :198-211); two test nits (the pull net-call check should
not depend on the -C path spelling; the warning/drops line order is
provisional until the text is final); then green, push, PR, CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit c9ffdce04615a196ddea7ce8b154efa52c97adf7)
…inds ahead of origin, so the RECOVERY's re-run of `commit` pushes the edit it stranded (#194)

`commit_push` returned at "nothing staged ... no commit made" before its
push loop. An edit the loop could not get out (a peer's uncommitted file
blocking the rebase, six rejected pushes) is already a local commit, so the
RECOVERY's re-run of `lanes-edit.sh commit` staged nothing, exited 0 and
pushed nothing. Now, with nothing staged, it reads origin/<branch>..HEAD:
empty is today's no-op with no network call; non-empty takes the same push
loop (the caller's lock, the dirty-elsewhere guard, the retries) without a
commit of its own.

The RECOVERY texts say what works: the dirty-elsewhere exit 3 says the
re-run pushes the local commits; refuse_dirty_checkout separates what the
authors commit from the refused act that is re-run; the conflict RECOVERY
lists every unpublished commit by content (`cherry -v`) and warns that
`reset --hard` drops a peer's unpushed commit along with the reader's own.
docs/README-lanes.md says the same in both places.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 9a29bbba1d636bb0e1a5dd1f439e65735e7a58fd)
…em 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>
(cherry picked from commit 941330ad73af6cce16f8204873b7a25d1d362104)
…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>
(cherry picked from commit 4d4957db6e093cb17faa5a61298d4e4d1c4ccf92)
…ipt, so `log` reaches refuse_dirty_checkout instead of the Amendment 7(b) session refusal

The first locked run showed case 6 failing on both trees for that reason,
before the checkout was ever read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 352c1b6f094e1c441cf2fd2ec0798e1f74bf5548)
…hanged; 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>
(cherry picked from commit 6658396e45ddeb125f21e48dbf2ce33126388224)
… staged finds ahead, and only lane tooling's own; the conflict RECOVERY lists HEAD and stops on a moved checkout

The review measured the blocker: with another lane's unpushed commit in the
shared checkout deleting `scripts/link-estates`, a no-op `replace-in-row`
published it and origin lost the file. Every write with nothing staged took
the push loop; now only `commit` does (CP_PUBLISH_AHEAD, set in its arm
alone and never read from the environment), and every other no-op write is
today's exit 0 with no network call.

`commit`'s publish reads every queued commit first and refuses with 2,
naming the commit and the path, when one deletes a file or touches a path
outside lanes/ and handoffs/. A failed read refuses the same way. Nothing is
pushed.

The conflict RECOVERY lists `cherry -v origin/<branch> HEAD`, which is what
the reset acts on, and adds a STOP for a checkout that moved after the
abort. It also says a LANES(concurrent@…) or LANES(pre-existing@…) capture
names no lane and is not the reader's. The dirty-elsewhere RECOVERY's claim
is qualified ("if lanes/LANES.md is clean") and names the refusal. The
publish list counts what it leaves out. replace_line's contract comment and
the manual say the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 836e1e1289341fa52678a822817b0faaf08fd1c9)
…iour before its wording, so a regression to publishing fails on the push, not on a note

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit fab2cbc57274522e174d9af079f3ed550bbe27a9)
…ch on the tree the PR will be judged on

origin/main at aaa7bc8 carries #191's install from the code an adopted
assembly pins (a74c7fc) and #227's Linux-only CI. A trial merge
(git merge-tree --write-tree) was clean; no file is changed on both sides:
main changed .github/workflows/tests.yml, AGENTS.md, README.md,
openRepoTools and four test modules, none of which this branch touches.
The merged tree's counts are in the commits that follow.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 7757eaab2b03612eac085755cfbb664fb53f0bf2)
…not 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>
(cherry picked from commit 9f23a6b098212efa2c839624f98ec68100d6e96f)
…readable, 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>
(cherry picked from commit bf2a220ae804aa963c301a5d0a8791f7f742a438)
…hes, refuses merges, type changes and HEAD off the branch, fails closed on every read, and pushes exactly the commit it read

The check found three ordinary checkout states where `commit` still
published a deletion of scripts/link-estates, and a race:
- a merge's own changes (`log --name-status` prints none);
- HEAD on another branch (the fence read HEAD, the push sent the branch);
- a local branch named origin/main shadowing the tracking ref;
- a commit made between the fence and the push.

cp_publish_fence now reads refs/remotes/origin/<b>..refs/heads/<b>,
spelled in full, and requires HEAD to be refs/heads/<b>. Its reads are
plumbing (rev-list --parents, diff-tree --stdin --always --raw) under
--no-replace-objects. It refuses merge and root commits, and passes only
an added or modified regular file under the register's derived directory
(LANES_PREFIX), its log and alias paths, or handoffs/.

Every read and every classification is status-checked, and diff-tree
must return one header per commit it was given, because it skips an
unreadable commit with exit 0.

The push sends <tip the fence read>:refs/heads/<b>. After each pull
--rebase, CP_AFTER_REBASE re-reads the queue and re-pins the push.
Ordinary writes push the branch as before.

Lane renames (a log deleted and re-added) stay refused, by design, and
the message says what carries them. Both exit-code tables list the new
refusal; the manual describes the rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit f34c993e317ccb0c7e5af9a5b3296d8635c8cacb)
Brings in #195 (aa14de3), #196 (983d8c7) and #212 (2732d59) on top of
aaa7bc8, so the T018 selection runs on the tree the PR will be judged
on. The only file both sides change is lanes-edit.sh: main's #212 edits
cleanup's temporaries and rename-lane's handoff stamp, the branch adds
project-legs and lane-reconcile's leg and paired-root reads. git merged
it with no conflict, the hunks are disjoint, and bash -n passes on the
result.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit cf7db4a77c5bf1fe6644c2caae351d59cebecd63)
…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>
(cherry picked from commit 5d2c1c8dc040664ee8af5aaffac656ecec860ab6)
…sis'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>
(cherry picked from commit 9a8f17549b4982d90951203b8a9d78ac0b147029)
…ing pushed", which test_the_exit_3_documentation_agrees_with_the_die_message_it_describes reserves

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5b68127fdde730fb49290a28f525f04e47dcd0ac)
Brings in 27b5c89 (lane-start's #138 fences) and 83d2690 (#199, the
handoff skill's commit block) on top of 2732d59. Neither touches a file
this branch changes: the two sides' file lists do not intersect, and git
merged with no conflict. The one cross-check that matters is
test_t018_the_legs_reader_is_lane_starts_rung_5_line_for_line, which
holds lanes-edit.sh's legs reader equal to lane-start's rung-5 lines:
those seven lines are byte-identical at 2732d59 and 83d2690. bash -n
passes on lane-start, lanes-edit.sh and lane-handoff.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 107958c7a6def94d994aa37e1c288942e2ccc534)
Brett Heap and others added 21 commits October 18, 2026 14:36
…(#198)

(cherry picked from commit ef16bbe0ae5b00094857aa747ed4bb3d065e54b2)
…nes/<lane>/, made on first use; a LANES_REPO naming W refuses; the locks name their holder's container and steal by claim; refresh-workspace fast-forwards W (Amendment 23 S1, S2, S5) (#198)

Act 2's slice 1a, written and not yet fully tested (a checkpoint: lane 2's
usage window, the coordinator's word).

What changed, in lanes-edit.sh:
- S2 routing. After the existing gates, the dispatcher works out the lane a
  write is attributed to (write_lane_of). Under default resolution the file
  re-runs itself by exec, same pid, with LANES_REPO/DIR/FILE at the lane's
  checkout and two internal markers (LANES__CHECKOUT_LANE, LANES__PID).
  Inherited markers are dropped. An explicit LANES_REPO is honoured in
  place, and LANES_DIR/LANES_FILE now default from it. Reads, dry runs and
  publish-handoff are not routed. A write that names no lane refuses (2).
- S1. lane_checkout_path/_verify/_ensure clone W's origin with
  --reference W --single-branch under a creation lock, through a .partial,
  then mv. Anything at the path that is not a clone of the register is
  refused (1), and nothing there is deleted or repaired. Also covered: the
  renamed-lane move, the old name reported, the row's spelling, W's
  repo-local identity copied, and the clone's own ten-minute budget.
- S5. A LANES_REPO whose real path is W refuses every write (2). A
  per-profile clone beside W is honoured with a warning naming S1's path.
  LANES_DIR/LANES_FILE outside the checkout refuse (2).
- The locks. The pid file is "<pid> <container> <host>". A live holder in
  this container is never aged out; another container's holder is judged
  by the lock's age, which git_net refreshes before every network call.
  The steal is a rename-claim that re-reads what it claimed. A lock is
  released only while it is still this process's. A lock taken before an
  exec is adopted only with the marker. The lifecycle, restart and tree
  sidecars and publish-handoff share one workstation lock,
  <W>-lanes/.lane-state.lock (ort186 Q3), and take no lock in W.
- New verbs: lane-checkout <lane> [--create] and refresh-workspace
  (fetch and merge --ff-only only; every block is reported and skipped,
  exit 2). Also the verb list, the exit-code table, the usage lines and
  the env doc.

Tests:
- New modules: test_a23_lane_checkout.py, test_a23_write_routing.py,
  test_a23_lock.py and test_a23_refresh_workspace.py. On this tree: 22, 21,
  15 and 10 passed.
- Existing fixtures that asserted writes in W now name their old W by the
  explicit seam, with W a reader copy (decisions D21, D24-D26 in the
  slice's progress record): lane-start's Sandbox, #214's Box, row-stamp's
  Sandbox, T018's Bench (a gitfile view, with W's path normalised), the
  two supervised-restart mutex cases (the sidecar lock), and the shell
  suite's head (it clears every inherited LANES_* and TMUX*, then sets
  LANES_REPO=$WIP; ort186 Q9) and its set-lane-tree wait.

WHY WIP — what has not run on this commit:
- The after-run of the existing modules on this tree stopped at the
  checkpoint, after the four new modules. In a scratch export with the
  same files, these passed: commit_push_194 25/25, lane_start_claude_current
  34/34, row_stamp 3/3, triad 5 passed 16 skipped and 1 passed 13 skipped,
  deferred_97 9/9, handoff_skill 24/24, rename_158 20/20, hardening_138
  20/20, pathspec 3/3, lane_worktrees 118/118, lane_worktrees_report 20/20,
  openrepotools_command 97/97, wip_init 63 skipped, and hygiene 189 and
  guard_launch_mode 13 on this tree.
- Not yet run against the new helper: test_supervised_restart_regressions,
  test_lanes_index, the install modules and test_claude_current.
- The one-line mutants are not run yet.
- The shell suite cannot be run in part (the repository gives no way), so
  it has not run.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit d0010a299d1773d9a941be11fe8ef480ff533d79)
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>
(cherry picked from commit 5dd0df07b6bcf8fd79f1dddc4c05b54d06da8e10)
…, #249) into act 2's slice 1a (#198)

(cherry picked from commit 465df5343483f945924c27b33559a961a28c2c4f)
(cherry picked from commit 129e201586caa64442dde1c8d803563d83254e3a)
…t, where its writes now go, so a new lane's second start and its end stop reading W's copy that no write updates (Amendment 23 S1, S2; act 2 slice 1a) (#198)

The bug: with slice 1a's routing, every write under the default resolution
goes through the lane's own checkout, <W>-lanes/<lane>/, and W moves only by
`refresh-workspace`. Both commands still read the lane's row from W's
working tree. A new lane's first start added its row through the checkout,
so the second start found a row on origin and none "in this checkout" and
refused at lane-start:1591 ("ALREADY has a row … refusing to add a second
row"). `lane-end` read the same W and said "the register has no row for
lane … nothing to end". Coordinator's ruling: add the minimal read fix to 1a.

The fix, the same block in both (`lane_register_follow`), once the lane's
canonical name is known and before its row is read:
- Under the default resolution (neither LANES_FILE nor LANES_REPO set), ask
  `lanes-edit.sh lane-checkout <lane> --create` and read that checkout's
  lanes/LANES.md. The checkout is made here if absent, as the first write
  would make it.
- `--dry-run` creates nothing, and reads W where there is no checkout yet.
- A helper that predates the verb writes W itself, so W is read.
- A refusal, a failed clone or the creation lock stops the command before
  it launches, renames or writes.
- Only the read moves. LANES_REPO, and the handoff lane-start publishes and
  commits, stay W's until slices 2a-3.

Also one line in each: a caller's LANES_REPO without LANES_FILE now reads
$LANES_REPO/lanes/LANES.md. That is where the helper writes since this
slice; it no longer reads W's register.

Tests: tests/test_a23_lane_start_read.py drives the real commands with no
seam. Four cases, each killed by its one-line mutant:
- a new lane's second start;
- lane-end after it;
- a dry run makes no checkout;
- LANES_REPO alone is the register read.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 51efdb39d6945477f29a0a1648fb4fd9a3f2adb9)
…second checkout by LANES_REPO and LANES_FILE, as the suite's other 24 cases do under Amendment 23's explicit seam (#198)

Main's fe58808 merge brought a new helper, r67(), that pointed the
commands at a second clone through LANES_WORKSPACE_ROOT alone. Since this
slice, the suite exports LANES_REPO and LANES_FILE at $WIP for every case
(ort186 Q9's seam). An explicit LANES_REPO is honoured in place and W no
longer chooses the checkout a command runs in, so r67's reads and its
refused `claim --force` would have run in $WIP, not the #67 clone. r67 now
names the clone the same way the other 24 cases were converted
(LANES_REPO=<clone> LANES_FILE=<clone>/lanes/LANES.md). Its assertions are
unchanged.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 59b39df22e6641cb8b5e5cbfd40f86e6a104b0ed)
(cherry picked from commit eff9107b7469f4483cb366b5477909c1387d149c)
… act 2's slice 1a (#198)

(cherry picked from commit fcf7f48578e57d7287e5496065ef21ee6bd696d5)
…tree is #214's final head's (bfbe9d2a), merged above (#198)

(cherry picked from commit 0d4ade89409ab4fe460158b95c0c0446263729b3)
… seam (LANES_REPO) and W becomes a view of it, so the indexer cases read the writes they make under Amendment 23's routing (#198)

The routed writes went to <W>-lanes/<lane>/, while lanes-index sync reads W's own origin/main. Four cases went red, against the base's two environmental failures. W is now a gitfile view of the writing clone outside the projects root, as in T018's bench. With it: 70 passed and 2 failed, which are the base's own two (the normaliser and the basetemp's uuid).

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit fda7808d64ee31520fa68b88314627916df03d0d)
(cherry picked from commit 6d22d0b53f077d2825b2e56e6f987ea58132b4cd)
(cherry picked from commit eb9eb7495e4e6c523869461580fbaa673b6588c2)
…ranch less this slice's four commits is its tree, aedf23d8 (#198)

(cherry picked from commit 9d26089ad28760b09f09717d09ae898ed2a9c39d)
…#271) into act 2's slice 1a (#198)

One conflict, in lanes-edit.sh's lane_state_follow, at the check of a
snapshot this helper cannot read: this branch releases the workstation's
sidecar lock (sidecar_release); main released W's mutex where the follow
took it (lsf_own) and gave a new note naming $LSS_WHY. Resolved as the PR
says: keep sidecar_release, take main's note. No line of main's that merged
on its own uses lsf_own or release_lock inside lane_state_follow, and main's
lanes-edit.sh changes since 55aae16 add no lock call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 18f5155163cad2f00302d4529755af4110ae12e9)
…1b, W follows a routed push, a W that is a lane checkout refuses, and a vanished lock is never made again (#198)

MUST 2: Amendment 23's routing is behind LANES_ROUTE_WRITES=1. Without
it every write goes through LANES_REPO (W by default) and every lock is
that checkout's, as on main: the sidecar acts and lane_state_follow take
$LOCK, and lane-start/lane-end read W. The two tests this slice had moved
onto the sidecar lock are main's again.

MUST 1: under the opt-in, once a push from a checkout that is not W has
landed, cleanup fetches W's origin/main (git_net, gc and maintenance off)
before the index nudge, so the prompt guard's offline read sees it. A
failed fetch is a note.

MUST 3: a W inside a `*-lanes/` directory, or the same checkout
LANES_REPO names under LANES_WORKSPACE_ROOT, refuses (2) and names
LANES_WORKSPACE_ROOT: lane-checkout and refresh-workspace always, writes
and sidecar acts under the opt-in. Nothing is cloned or locked inside a
lane checkout.

MUST 4: git_net's heartbeat is `touch -c`, and the creation lock too,
which a heartbeat now refreshes every minute through the clone (NIT 5).

MUST 5: test_register_scan_hardening_138.py writes through `wip` by
LANES_REPO, with W a clone of its own.

SHOULD 1: append-line attribution reads the published register first,
and a routed arm falls back to the lane it was routed by. SHOULD 2:
lane-start/lane-end note a checkout that cannot be made (2, 4) and read
W. SHOULD 3: S5 compares the checkout's root and git common directory.
SHOULD 4: a routed write refuses while W's register holds an uncommitted
edit. SHOULD 5: lane-worktrees reports `<W>-lanes/<lane>` as a register
checkout, never FOREIGN. SHOULD 6: lane-start's carry note says no write
carries a commit made in W under the opt-in.

NITs: 1 request-handoff's `--` ends the options in the router too; 2 the
sidecar lock's wording (taken alone, never inside the checkout lock); 3
mutex_release compares container and host as well as the pid; 7
lane-end's own "Nothing was written"; 8 LANES_NO_GIT=1 makes no clone; 9
an explicit LANES_REPO still needs a lane.

Sonar: the four copies of the test Box are one module, tests/a23_box.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 874e0204192902554ffece916632a613fcff0627)
…ates, the lane-checkout home check, the creation lock's `touch -c` and heartbeat, lane-end's notes, S5's git-directory clause and the alias name check (#198)

The mutation run over 874e020 let 13 mutants of fix round 1 survive its own
tests. These 12 cases (15 test items) kill 12 of them; the 13th, N26 (S5's
`--show-toplevel` compare), is equivalent, because a path whose toplevel is W
also has W's git-common-dir, so the common-dir clause already refuses all it
refuses, and no test can tell the two apart. Each case is appended to its
module unchanged; no helper, fixture or other test is touched.

tests/test_a23_lock.py
- test_without_the_opt_in_a_lane_windows_sidecar_act_works_as_on_main kills
  N03b (sidecar_lock_use is a no-op without the opt-in).
- test_a_sidecar_act_in_a_W_that_is_a_lane_checkout_refuses[both, root-only]
  kills N09 (sidecar_lock_use runs lanes_home_check).

tests/test_a23_lane_start_read.py
- test_without_the_opt_in_lane_end_reads_W kills N05 (lane-end reads its lane
  checkout only under the opt-in).
- test_a_checkout_that_cannot_be_made_is_said_and_the_end_goes_on kills N21
  (lane-end notes a checkout that cannot be made and reads W).
- test_a_held_creation_lock_is_said_and_the_start_goes_on kills N22
  (lane-start's note arm takes the creation lock's 4 too).
- test_a_held_creation_lock_is_said_and_the_end_goes_on kills N23 (lane-end's
  note arm takes the 4 too).

tests/test_a23_refresh_workspace.py
- test_refresh_refuses_a_W_that_is_a_lane_checkout[both, root-only] kills N11
  (the refresh-workspace arm runs lanes_home_check).

tests/test_a23_lane_checkout.py
- test_a_creation_lock_that_vanished_is_not_made_again kills N13 (the creation
  lock's first touch is `touch -c`) and N14 (so is the heartbeat's).
- test_the_creation_lock_is_refreshed_while_the_clone_runs kills N15 (a
  heartbeat touches the creation lock while the clone runs).
- test_a_name_the_log_cannot_carry_is_refused_though_an_alias_maps_it
  [repoCK/1, repoCK 1] kills M31 (a name the log cannot carry is refused
  before any checkout, and before canon_lane reads with it). It adds
  `import time` to the module's import block.

tests/test_a23_write_routing.py
- test_S5_refuses_a_checkout_whose_git_directory_is_W kills N25 (S5's
  git-common-dir compare).

tests/test_register_scan_hardening_138.py
- test_item_9_refuses_in_the_scan_under_the_opt_in_too kills N16 (the fixture
  writes through wip by the seam).

Runs (every inherited LANES_* and TMUX* unset, one module at a time), before
-> after: test_a23_lock.py 19 -> 22 passed; test_a23_lane_start_read.py 6 ->
10; test_a23_refresh_workspace.py 10 -> 12; test_a23_lane_checkout.py 22 ->
26; test_a23_write_routing.py 34 -> 35; test_register_scan_hardening_138.py
28 -> 29; test_repo_hygiene.py 237 passed; test_test_roots.py 39 passed,
1 skipped.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit ecb96eda1e3d38cdf139659829d45366722e7d52)
…s stale at ten minutes as on main, only `commit` refuses over a hand edit in W, a write for a lane with no row clones nothing, and the daily report knows a lane window's register checkouts (#198)

Answers lane openRepoTools-3's re-review of 874e020 (N1-N5, SHOULD 5) and
its re-check of the 12 cases at ecb96ed (R3-1, R3-2).

N1: mutex_stale. Without LANES_ROUTE_WRITES=1 the lock is W's, shared by
every lane, and a live holder in this container whose lock is over ten
minutes old is stale, as main's lock_try has it; git_net's `touch -c`
heartbeat keeps a working holder fresh. Under the opt-in a live holder is
still never aged out (S2). refresh-workspace's note on a stale lock in W no
longer calls it "an older helper's leftover". The env doc and the lock
comments say so.

N2: ws_pending_refuse refuses only `commit`, the verb that publishes a hand
edit. Every other routed write says the same as a WARNING and goes on; the
edit stays in W, untouched and captured by nothing. hardening_138's routed
sweep case pinned the refusal N2 removes: it now warns and captures nothing.

N3: that check's `git status` runs with GIT_OPTIONAL_LOCKS=0, so it never
rewrites W's index under W's index.lock.

N4: lanes_home_check calls a checkout both variables name W itself, not a
lane checkout, where it is workspace.yaml's own `path:`. Still exit 2 (S5).

N5: lane_row_gate. Where a lane has no checkout yet and the verb's arm needs
exactly one row of it (set-row-state, replace-in-row, append-session-id, and
rename-lane by its old name), a lane with no row on origin refuses (2) and
nothing is cloned. A miss is read again after one log_sync of W; a read that
could not be made, or a fetch that did not run, refuses nothing. log, claim,
release, commit, append-line, request-handoff and the sweeps take a lane
with no row (an archived lane keeps its log), and add-row makes the row.

SHOULD 5: lane-worktrees takes the parent of real(W) as the lanes home where
that parent is a `*-lanes` directory, as lanes_home_check reads one.

R3-1: the two lane-end cases assert "in-flight read from lanes/log/", which
lane-end prints only after the follow-up (kills lane 3's n21b).

R3-2: Sandbox.start takes a timeout (default 120, unchanged elsewhere); the
held-creation-lock start and end get 300 s; the creation-heartbeat case
waits up to 30 s for the first beat instead of a fixed 4 s.

New tests: test_a23_lock.py
test_without_the_opt_in_a_live_holder_is_aged_out_at_ten_minutes (2);
test_a23_write_routing.py
test_a_hand_edit_in_W_warns_every_other_routed_write_and_stops_none,
test_the_hand_edit_check_takes_no_lock_in_W,
test_both_variables_naming_W_itself_refuse_and_say_it_is_W,
test_a_write_that_needs_a_row_clones_nothing_for_a_lane_with_none,
test_a_row_W_has_not_fetched_is_no_reason_to_refuse (2);
test_lane_worktrees_report.py
test_a_W_that_is_a_lane_checkout_lists_its_siblings_as_register_checkouts.

Runs (one module per pytest process, every inherited LANES_* and TMUX*
unset), ecb96ed -> this commit: test_a23_write_routing.py 35 -> 41 passed;
test_a23_lock.py 22 -> 24; test_register_scan_hardening_138.py 29 -> 29;
test_lane_worktrees_report.py 21 -> 22; test_a23_lane_checkout.py 26 -> 26;
test_a23_refresh_workspace.py 12 -> 12; test_a23_lane_start_read.py 10 ->
10. Also at this commit: test_lane_start_row_stamp.py 3 passed;
test_lane_worktrees.py 118 passed; test_wip_init_command.py 63 skipped;
test_repo_hygiene.py 237 passed; test_guard_launch_mode.py 13 passed;
test_test_roots.py 39 passed, 1 skipped. The new tests against ecb96ed's
scripts: 8 failed (N1 x2, N2, N3, N4, N5, the sweep case, SHOULD 5), and
the 6 that pin the head's behaviour passed.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit eead86da50ee351e86918cc10ca647737c244424)
…n, `refresh-workspace` and `lane-checkout` no longer refuse a W that both variables name, because nothing is routed and a write goes through W as on main (#198)

Answers lane openRepoTools-3's re-check of eead86d (6301653436), the one
item that stopped slice 1a, R4-2.

R4-2: lanes_home_check's "W itself" branch (both LANES_WORKSPACE_ROOT and
LANES_REPO naming the path: workspace.yaml names) is S5's refusal, and S5
exists only under LANES_ROUTE_WRITES=1. The branch ran unconditionally from
`lane-checkout` and `refresh-workspace`, so without the opt-in
`refresh-workspace` exited 2, naming an opt-in that is off, for a verb that
writes no register; and exit 2 is its own "skipped, nothing changed", so a
caller went on with a stale W. The first line of the branch is now
`[ "$LANES_ROUTE_ON" = 1 ] || return 0`, lane 3's fix. Under the opt-in the
refusal and its words are unchanged, and a W that is a lane checkout is
still refused with or without it. The other two callers (the sidecar lock
and lane_checkout_enter) run only under the opt-in already.

Tests: tests/test_a23_refresh_workspace.py
test_without_the_opt_in_both_variables_naming_W_still_refresh_it (exit 0 and
W fast-forwarded, no refusal), and tests/test_a23_lane_checkout.py
test_without_the_opt_in_both_variables_naming_W_are_no_refusal
(`lane-checkout` answers 8, nothing made). Both fail at eead86d, with exit 2
and S5's words, and pass here; the N4 case in test_a23_write_routing.py
still pins the refusal under the opt-in. R4-1 is slice 1b's and is not
touched.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 15c8e3f813ed26ee123dfff8b91bd77f51433952)
…'s #198 comment 6341440689 suggests

The line is the `exec "${BASH:-bash}" "$RESOLVED" ...` at the end of the routing path in `lane_checkout_enter` (15412), which shellcheck 0.9.0 reports as SC2093 with the baseline flags of `lint.yml` on #277's branch (`ci/actionlint-shellcheck-job-277` at 935eaef): `--severity=warning --exclude=SC1007,SC2034,SC2069,SC2088,SC2128,SC2141,SC2154,SC2178,SC2209,SC2318`. The `exec` is deliberate, so it now carries `# shellcheck disable=SC2093` and a two-line reason (it hands this one write to the routed checkout's copy and does not return, and the `die` after it runs only if the `exec` fails). Before: 15c8e3f gave exactly SC2093 at 15412 (exit 1) and main's 33ce26d was clean (exit 0). After: this file is clean (exit 0, nothing reported). Nothing else changed.

Refs #198

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 6ca31581c4410e736ce78da373a12388e94da939)
…code PRs 3 to 7) into the code-leg slice 1a (opensoft/openRepoTools#198)

Main moved 22 commits past 0cefa1f, the code leg's rewrite of f18b818 on
which this slice stands (6 of them are merges). The stack takes the code
leg's main, with T017's five lanes-edit.sh fixes:
- 4393d79, 9add651, 4dd0fcf, 4e3eebe, 09b5355 and d9e7d56 are six of the
  old main's eight commits since f18b818, as the code leg holds them (the
  other two, the v1.1.0 notes and T010's assembly job, touch only docs/ and
  specs/, which went to the spec leg):
  tests/test_release_workflow.py runs the release steps
  (opensoft/openRepoTools#284); the shell suite's wrapper prints its timing
  table on a pass (opensoft/openRepoTools#265); the register's rewrites
  refuse a copy or write they cannot prove (opensoft/openRepoTools#263); a
  lane listing that is not whole no longer removes indexed worktree rows
  (opensoft/openRepoTools#289); under set -e, macOS's bash 3.2 no longer
  ends lanes --worktrees, wip init or lane-end where a utility run through
  command fails (opensoft/openRepoTools#294); CI and the release workflow
  run on ubuntu-24.04 (opensoft/openRepoTools#296);
- 6314fc8, bc7c831, 6c53c9e and 1e6f2d1, merged by 2a057a6 (code PR 1), are
  the cutover's own: the code leg's front door (AGENTS.md, CLAUDE.md,
  README.md, the LF rule, the ignore list and the licence), the tests
  workflow's standalone checkout context, the assembly root's two length
  caps, and the shell suite's lane-start row-stamp cases;
- 609ad61, c23f535, 70632d3, e527b5d and 577110e are code PRs 3 to 7,
  T017's five lanes-edit.sh fixes: a lane dir with a space is listed, a TAB
  or backslash in a recorded dir reads back as written, a control root that
  cannot be searched is UNREAD, a tree record with no absolute path makes
  its lane UNREAD, and a named worktree read says UNREAD instead of none.

Why: this slice was rebuilt on the code leg from old 6ca3158 and stands on
0cefa1f, from before the cutover and before the ubuntu-24.04 pin d9e7d56, so
it would not carry the code leg's front door or its five fixes. The 1b, 2a,
2b, 3 and 4 slices take the code leg's main through this one.

The merge is clean: no hunk was resolved by hand. git merge-tree of 577110e
into 4d6bc14 gave tree de12f63 before the
merge was made.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
….1's pin) into the code-leg slice 1a (opensoft/openRepoTools#198)

Main moved from 577110e to e393958 when #15 landed
at 19:30:50Z: the pin of upstream/openRepoShape moved from 190133d (v1.1.0)
to 5017830 (v1.1.1). The merge brings five files, +12 -8:
- upstream/openRepoShape: the submodule pointer, 190133d -> 5017830;
- contracts/openreposhape-pin.yaml: the pinned commit and its tree_sha256;
- .github/workflows/release.yml: the header says the pin moved, and keeps its
  v1.1.0 provenance (comment lines only);
- tests/test_release_workflow.py: the MODEL comment says the same (comment
  lines only);
- tests/test_install_payload_source.py: one docstring names the pointer as
  openRepoShape:745 at 5017830.

Why: this slice lands alone, by merge commit, as #16.
Its landed tree must equal a tree CI tested green, so the slice takes the new
main in place and is tested there. Slice 1a stood on 577110e, the previous
main, and would otherwise be tested on a main that is no longer the one it
lands on.

The merge is clean: no hunk was resolved by hand. git merge-tree of e393958
into 903d751 gave tree 14d48cf before the
merge was made.

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

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant