Skip to content

The name guard refuses a prompt whose window, session name and register row are not ONE lane — the lock types /rename <lane> into the pane, a person's rename is an OFFER answered at the next prompt, and a transcript two live processes hold is a refusal in the guard, in lane-start and in lane-end --retire - #52

Merged
brettheap merged 14 commits into
mainfrom
feat/amendment-12-name-guard-lock
Sep 15, 2026

Conversation

@brettheap

@brettheap brettheap commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openRepoTools-3

← brettheap/new-workstation#22 (Amendment 12, ratified 2026-09-13) and #34 (Amendment 18 clause (h), ratified 2026-09-14)

A lane session works only while three names are one — the tmux WINDOW it runs in, the SESSION's own name, and the register ROW whose session cell ends on this transcript — and otherwise the prompt is refused. Brett Heap's ruling, in session, 2026-09-13, verbatim: "we need the session name and the lane name always the same. we need a gaurd on this and is must stop and refuse to work if they are not the same"; ratified the same day as "Ratify revision 2", with M1 "Type /rename into the pane".

Amendment 2 has said since 2026-09-05 that a lane is its window's name and its session's name. Nothing refused anything for eight days, and between 2026-09-10 and 2026-09-12 THIS lane ran as openRepoTools-3, -4, … -14 — a fresh title per /resume, in a window still named claude, in profiles that changed at every usage reset — and no row was ever written. session_start_block says which lane a window is and "ALWAYS exits 0"; window_session "decides nothing". This is the surface that decides.

The acts

1. lanes-edit.sh guard — the UserPromptSubmit hook. Reads the hook JSON on stdin; exits 0 silently when cwd is outside $PROJECTS_ROOT (D1), when the input carries agent_id, or when the three names agree; otherwise exits 2, which is what blocks a prompt, printing the triple as it stands and the ONE cure, filled in. One row of the (b) table per state, every message in the register's own spelling (Amendment 15):

found done
window not a lane, session name parses as <repo>-<n> lane-start --no-launch <repo> <n> — the 2026-09-10 case
neither name a lane refuses; which lane this work is is the person's to say
window is a lane, this uuid is the row's last, session name is no lane name THE LOCK RENAMES IT — types /rename <lane> into this pane
… differs from the row only by case the lock renames it to the ROW's spelling (Amendment 15)
… is ANOTHER lane's, nameSource=user, newer than the binding THE OFFER (D5)
uuid IN the cell but not last a SUPERSEDED transcript: lane-start <repo> <n>
uuid in NO row the recording act: lane-start --no-launch <repo> <n>
two rows differing only by case refuses naming both and 15(d)'s merge
not inside tmux refuses (D2)

THE LOCK types /rename <lane> into the session's own pane with tmux send-keys — M1's mechanism, and the only path a running session's name has — refusing that one prompt so the rename lands first. The pane's #{pane_current_command} is asked FIRST and nothing is typed into a pane running anything else; every other outcome prints the line for the person. A <lane> (N) title is a mismatch (D4) and the suffix is evidence a stale holder was live: the note names lanes-edit.sh forks and lane-end <lane> --retire <pid>, never a kill (Amendment 11 clause (k) rule (e)).

THE OFFER writes $CLAUDE_CONFIG_DIR/lanes/offers/<uuid> and asks. The next prompt is the answer, consumed and never reaching the model: yes renames the window FIRST — lane-start's step 3b veto 2 refuses a session in a window named for another lane, so without that order the lane would be started and this transcript left out of its cell — then runs lane-start --no-launch <repo2> <m>, appends this uuid to the new lane's cell (the section below says why lane-start may not), and writes MOVED → <repo2>-<m> on the lane it left; no types /rename <old lane> back; anything else re-offers.

FAIL CLOSED (clause (d)): a mismatch refuses, and so does an indeterminate read — records unreadable, tmux silent, register unreadable, a payload naming no cwd — naming the read. The row is read from origin/<branch> as this checkout last had it (R19) and the guard never fetches. --safe-mode is the one bypass and is named by the indeterminate refusals.

1b. Amendment 18(h) — one live process per transcript. lanes-edit.sh transcript-holders <uuid> sweeps every profile's sessions/ (a duplicate's record sits in another profile's — measured four times on 2026-09-14, the last at 18:14Z), matches a window by id AND session name (Amendment 11(h): tmux reuses ids), and gives each live process a verdict of here / companion / duplicate / unrelated. Three surfaces share the one read rather than implementing the rule three times:

2. lane-start --name "$LANE" on every launch case — ALREADY DONE, and nothing was added. Amendment 11's hotfix R-A11-1 did it: lane-start:1781 (resume by id), :1803 (resume by title), :1814 (new session), with the argv asserted at tests/test_lane_helpers.sh:698, 715, 730, 766, 772, 788, 796, 804, 819, 888, 1937, 1955, 2221, 2240, 2259, 2466, 2516 among others. Verified, reported, and left alone.

3. openRepoTools --install places the second entry — ~/projects/xFactory/lanes-edit.sh guard, timeout 5, no matcher (UserPromptSubmit has no source to match on) and no || true (on SessionStart that suffix is the whole safety property; here it would exit 0 for every code the command can produce, installing a guard that refuses nothing, silently). Both merges are computed into one staged file before either is placed, so the all-or-nothing rule covers the pair. The UserPromptSubmit arm keys on the verb guard with a boundary on both sides, not as a substring: ~/projects/xFactory/guard.sh is this estate's own usage guard, in every profile, and keyed loosely it would make every install refuse.

4. session_start_block gains clause (f)'s line — session name '<x>' was not the lane '<y>' — renamed — and types the rename itself ((h)1: its ONE act on a pane). Where another live process carries this id the block SAYS so and names the retire act. Still read-only against the register, still non-blocking, still always exit 0.

5–6. Tests and docs — below.

7. The hook is NOT installed live on Eagle here. That is adoption act 4, Brett Heap's first-live-case act, after this lands.

What the resume found, and it was one line

This branch began as five modified, uncommitted files from a session killed mid-work; 3befe7c records those bytes unchanged, before anything was done to them. No suite had ever been run against them, and the first run said why that mattered: transcript_holders never initialised th_here_prof, lanes-edit.sh runs under set -u (line 292), and pass 2's [ -z "$th_here_prof" ] is reached for the first record that is not this window's. 1468 passed, 124 failed — the WHOLE of the new Amendment 12 section, plus every lane-start case reaching step 3b's window binding, because dup_check reads that 1 as "the records could not be read" and refuses fail-closed.

An unbound variable under set -u exits the shell where it stands, so || : and || rc=$? at the call sites caught nothing, and the two hooks failed in the two directions neither may fail in:

  • session-start exited 1 and printed no block at all — the hook breaking the session it exists to orient (R-A8-1);
  • guard would exit 1, and a 1 does not block a UserPromptSubmit — only a 2 does. A fatal inside the guard let the prompt THROUGH, in silence, which is the one failure clause (d) forbids by name.

So the fix is not only the initialisation. The guard dispatcher arm runs guard_run in a subshell and maps every code but 0 and 2 to a 2; ssb_name_line runs transcript_holders in one and session_start_block calls ssb_name_line in one. Everything that must cross a subshell does — the offer file, lane-start, the register write, stderr — and the only thing lost is shell state nothing reads afterwards. Both properties are now asserted directly: the same unreadable-records fixture is given to both hooks, and the guard is given a register it cannot read and must answer 2, never the dispatcher's 1.

And one thing the suite found that no test was written for

After a yes, lane-start --no-launch cannot put this uuid in the new lane's cell, and the guard would then refuse for ever. Its step 3b veto 1 — Amendment 11 clause (d) rule 1 — never takes a uuid that belongs to ANOTHER row, and after the rename this uuid still belongs to the lane being left. Measured here: yes moved the window to repoGD-2, whose row then read STARTED by 7a01ae69… while the session was aaaa0012-1111…. The NEXT prompt therefore finds a lane window whose row does not name this transcript — the last row of the (b) table — and refuses with lane-start --no-launch repoGD 2, which vetoes for the same reason and changes nothing. A blocking hook that refuses for ever, on a state it created by obeying the person, is the worst outcome this surface has.

Clause (h) rule 2 names the end state and rule 3 repeats it — "window X, session X, row X stamped with this uuid" — so after lane-start returns, the guard makes that one write itself, with lane-start's own anchor discipline (the PUBLISHED last id, so a stale copy of the row refuses rather than writing a cell that no longer matches). The veto is right and stays: it exists for the take nobody asked for — "lane-start openXfactory-5 typed from a window named openRepoProject-1", Evidence 2(b) — and clause (h) rule 4's own limit is that the lock never moves a uuid between rows without the person's yes. Here there is one, on the record, answered at this very prompt. The suite asserts both halves: the cell ends on this transcript, and the guard SAYS it made that write.

Counts

bash tests/test_lane_helpers.sh
main @ 92eab6c (this branch's base) 1500 passed, 0 failed
main @ 8345d2f (with Amendment 15, #41 — the merge base now) 1678 passed, 0 failed
this branch at the predecessor's state 3befe7c 1468 passed, 124 failed — unverified, never run by its author
this branch after the set -u fix, before the fixture and veto rounds 1750 passed, 20 failed — all 20 in the new section, nothing else red
this branch before Copilot round 1 (109186b) 1777 passed, 0 failed — 99 assertions the new section adds, and nothing else moved
this branch at eedb55f (round 1 taken, the two (b) rows that had no case) 1815 passed, 0 failed
this branch at 6cf5359 (round 2 taken) 1833 passed, 0 failed
this branch at e2b6694 (round 3 taken, three merges of main, the macOS fixture fixed) 1870 passed, 0 failed — every CI check green here, tests-macos included
this branch, head 9059f38 (round 4 taken) 1875 passed, 0 failed — and every CI check green here too, tests-macos in 23m39s

python3 -m pytest tests -q: 619 passed, 0 failed at the head, in 1231 s, on this workstation under the serialization lock (578 at 109186b, in 1103 s, before the three merges brought main's new tests) — which includes test_lane_helpers_suite.py running the whole bash suite a second time. At eedb55f every CI check passed — tests (the whole pytest suite) in 12m15s, tests-macos in 23m58s, tests-no-submodule, tests-windows and SonarCloud — which is the same suite this workstation's one pytest lock kept queueing locally (two local runs were killed from outside this worktree, at 144 and at 137). Seven test ids added, all in tests/test_install_skill_and_hook.py; every other change is to an existing case's docstring, its count or its fixture, so main @ 8345d2f collects 571.

The artifact count moves 18 → 19 everywhere it is derived or stated, because --install now merges TWO entries into one settings.json: derived in tests/test_openrepotools_command.py (ARTIFACTS = len(INSTALLED) + 2*len(SKILL_NAMES) + 2*len(COMMAND_NAMES) + HOOK_ENTRIES, the hand-kept + 1 now a named HOOK_ENTRIES = 2), stated in openRepoTools (six places, including "THE SEVEN ARTIFACTS THAT ARE NOT ONE OF THE ELEVEN FILES" → EIGHT), README.md and docs/README-lanes.md.

Tests

tests/test_lane_helpers.sh gains == Amendment 12: the name guard and the lock ==, placed before the Amendment 15 section so == the workstation seam == stays second-to-last. One case per row of the (b) table, fed a real hook payload on stdin: the agreeing case silent, exit 0, nothing typed; an out-of-scope cwd and an agent_id payload the same; the lock typed once and the logged send-keys -t %12 /rename repoGD-1 Enter asserted; <lane> (N) with the stale-holder note and no kill anywhere in it; a lowercase window resolving to the canonical row and every message and typed command carrying the register's spelling; both cell positions; two rows differing only by case; unreadable records; a payload with no cwd; the offer written, yes running lane-start --no-launch and writing MOVED →, no renaming back, a third answer re-offering; two live records on one id (one bg, in another profile's sessions/) refusing the prompt, refusing lane-start's bind, retiring under --retire <bg pid> and refusing --retire <window pid>; and clause (f)'s line typed by a SessionStart hook that still exits 0.

The sandbox's fake tmux gains two columns (%pane, the pane's current command) and two formats (#{session_name} for the agreement rule, #{pane_current_command} for M1's condition), logs send-keys as it always logged rename-window, and takes an opt-in sticky rename — opt-in because #W has been a constant for every case written before this one. The section captures the three FAKE_TMUX_* variables at its top and restores them at its bottom, so the Amendment 15 section below reads exactly what it read against main.

pytest: seven cases in tests/test_install_skill_and_hook.py — the entry byte-identical and carrying no matcher, the absent || true asserted on its own (copying the neighbouring string is the obvious way to write this and would pass every other test in the file), twice adding one entry, a differing guard entry refusing and placing nothing, ~/projects/xFactory/guard.sh not a conflict, the pair as ONE all-or-nothing merge (the conflict on the guard entry alone, so the cleanly-mergeable SessionStart entry must not be placed either), and one write placing both.

Docs

docs/README-lanes.md gains "The name guard and the lock (Amendment 12)": the triple, the table filled in, the lock and its pane conditions, the offer and its three answers, Amendment 18(h)'s duplicate read and its three callers, clause (f)'s line, fail-closed with the one bypass, and the scope. README.md's install paragraph names the second entry, what it does, and that a person's own UserPromptSubmit hooks are left where they are; cap 409 → 417 with a dated entry. AGENTS.md's lane paragraph promised, without qualification, that each of the four "refuses with exit 1 … where that file does not answer" — and guard deliberately answers 2, for the reason above; the sentence now carries the exception. Cap 224 → 228, also dated.

Notes for the reviewer

Copilot round 1 — four taken, eight declined

Round 1 arrived 2026-09-14T20:45:53Z: 7 inline threads and 5 findings suppressed into the review body, effort level Lite. Sourcery gave no review at all — its 7-day diff-character budget is spent, and it says so in place of one. Four findings are real and are taken in eedb55f; two of those four are checks that could not fire at all, which is the worst shape a fence has.

Taken

  1. lane-start's row-resume branch could never refuse (lane-start:1415). 18(h) names that branch first — "the id it is about to RESUME or bind" — and it is reached only when row_sid != win_sid, so this window carries no record of the id being resumed and every live holder of it comes back unrelated, which dup_check dropped. unrelated now blocks in dup_check and nowhere else: the shared verdict keeps it silent so a rule about a transcript cannot overturn Amendment 8(f)'s orphan or forks, both of which ask about a LANE, while dup_check asks 18(h)'s own question about ONE id. At the window-binding call that id IS this window's session, so a here record exists and nothing is unrelated.
  2. The companion is the bg record, not any record in this profile (lanes-edit.sh:5463). Amendment 8 ruling (g) names exactly one thing to pass over — "kind: bg, no tmux, no nameSource, its own pid, and THE SAME sessionId" — and keyed on the profile alone a second live INTERACTIVE record beside this window's own read as that companion. One profile resuming one transcript twice is the cheapest duplicate there is: no move, no swap, one --resume. record_fields_are_session is now asked here, so bg is a companion and interactive is a holder, in one reading for all three surfaces.
  3. An answer is a prompt (suppressed, lanes-edit.sh:5758). The offer's yes/no was consumed in front of 18(h)'s count; a yes runs lane-start, writes a row and appends a uuid to a cell — out of a process that may not be writing at all, on an offer file the other process could equally have answered. The refusal now stands in front of the answer and the offer is kept, to be answered at the first prompt after the duplicate is retired.
  4. The offer never runs a command it could not fill in (suppressed, lanes-edit.sh:6023). lane_start_args answers --dir <path> <lane> for a lane named before Rule 4's <repo>-<n> form — the register carries 22 of them — and <path> is a placeholder. Printing it in a diagnostic is honest; running it is not. The offer says what it cannot fill in, yes refuses with the act and the one word left to the person, and the offer is kept so no still answers.

Declined, one line each

  • lane-end should read ownership from the here verdict (lane-end:684) — here is relative to the process ASKING: lane-end <lane> --retire <pid> is typed from wherever the person stands, and run from any window but the lane's nothing is here, so ownership-by-here would name no owner and the refusal lane-end --retire refuses a live duplicate process of the row's OWN session id (a --fork-session the launcher named as a fork before lane-start appended its id) — the retire act had to be a bare kill #39 asks for — the window's own session — would vanish exactly when the act is most likely to be used.
  • --install reports present before it looks for a rival (openRepoTools:708, and the same at :722) — the shape predates this PR on the arm it is reported on (SessionStart is main's, under R-A9-8 and R-A9-14) and the guard arm mirrors it byte for byte; in that state --install adds nothing at all, so nothing is installed twice by the run. Whether conflict should beat present is one decision for both arms: filed as --install's hook conflict read answers present before it looks for a rival, so a settings file carrying BOTH our entry and another writer of the same hook installs in silence (both arms) #59, claimed by this lane.
  • An unreadable register could produce a rename or a move (lanes-edit.sh:5717) — it cannot: both the lock and the offer sit behind [ -n "$gr_last" ] && [ "$G_ID" = "$gr_last" ], and a read that produced no rows produces no gr_last, so the prompt is refused by the table's last arm. What is wrong is the DIAGNOSIS, not the direction: filed as The name guard's two remaining indeterminate reads pick a branch of the (b) table instead of naming the read: the register's TEXT and the lane's binding log #60.
  • A failed binding read authorises the move offer (lanes-edit.sh:5850) — it changes which REFUSAL the person gets and never whether the prompt runs: false → the lock (refuse, rename back), true → the offer (refuse, ask). Reading an unreadable log as "not newer" would silently rename a person's own /rename away, which is what clause (h) rule 4 forbids; asking is the conservative act. The message should say the log could not be read: The name guard's two remaining indeterminate reads pick a branch of the (b) table instead of naming the read: the register's TEXT and the lane's binding log #60.
  • guard_bind_uuid's failure should hold the MOVED write and keep the offer (lanes-edit.sh:6031) — by that line the move HAS happened (lane-start returned 0: window renamed, row created, lane stamped), so MOVED → on the lane left is true; keeping the offer would consume the person's next prompt as an answer to a question already answered, and guard_bind_uuid prints the exact append-session-id act, which is Amendment 6(c)'s own remedy.
  • A record that becomes unreadable mid-pass is silently skipped (suppressed, lanes-edit.sh:5422) — an unreadable record is already fail-closed one line above: xargs -0 grep -l … 2>"$th_err" and any stderr at all sets SESSION_FILES_ERR and returns 1. What the cat … || : passes over is a file that VANISHED between the grep and the read, which is a session that ended, and a record that is gone is not a live holder.
  • under_projects_root's indeterminate answer should refuse (suppressed, lanes-edit.sh:5632) — that read answers clause (e)'s SCOPE, not the triple. This hook runs at every prompt of every session on the workstation, most of which are nobody's lane; refusing every prompt of a session we could not prove is ours is a larger failure than missing the guard in a session whose own cwd no longer exists. A $PROJECTS_ROOT that does not resolve is itself proof of "outside" — nothing is under a directory that is not there.
  • The guard-entry conflict ordering (suppressed, openRepoTools:726) — the same finding as :708 above, same answer, same issue.

Two assertions changed, both with the citation and neither weakened. Amendment 8's lane-start does not refuse a lane over an orphan and …so it cannot refuse a lane either (a lone bg companion) now expect 1: the act that run makes next is claude --resume <the row's id>, and a live process already carries that file. Everything else those two cases assert is untouched and green — the orphan is REPORTED, named "orphaned holder, not a rival", given 6(d)'s act, and the rival refusal (take repoGH-2) is still absent, so the LANE is still not taken by it; what is refused is the second resume of its TRANSCRIPT, which is 18(h)'s own sentence. 8(f)'s rule does not reach that state either: it governs "a row's EARLIER session ids … still held by live but idle sessions", and this is the id the row ENDS on. Two has lines are added to each case, so the refusal they now take is asserted rather than assumed.

And two rows of the (b) table that had no case, found by reading the table against the section rather than by the review: not inside tmux at all (ratified decision D2) and a tmux that answers nothing, which is clause (d)'s indeterminate read and a different state with a different message. The first is the one cure in the table that cannot be filled in — which lane a windowless session belongs to is exactly what nothing there knows.

Copilot round 2 — four taken, four declined

Round 2 arrived 2026-09-14T21:53:00Z on the round-1 push: 3 inline threads and 5 findings suppressed into the review body, and it was NOT a restatement — two of its findings are in code round 1 wrote, which is the argument for reading a second round at all. Taken in 6cf5359; every thread replied to and resolved.

Taken

  1. not a session record was too wide a test for "the harness's companion" (lanes-edit.sh:5485), and the counter-example is in this suite already: Amendment 11 clause (k)'s section writes an interactive fork with kind: user. Under round 1's spelling that record — and every kind a later harness invents — arrived at the guard as the benign bg companion, and two live processes on one transcript went unrefused. Ruling (g) names ONE shape to pass over, so the test is now kind = bg; everything else live on this id is a duplicate, including a record too old to carry a kind at all (pass 1 writes that down as interactive).
  2. lane-end --retire <pid> selected its target by pid and never read the verdict (lane-end:691), so the one pid a person can reach for that is neither a duplicate nor the lane's own session — the harness's kind: bg COMPANION of that very session — was reported as a live duplicate and given Amendment 6(d)'s ending. It is the other half of the conversation that IS the lane, and it is now refused by name in ruling (g)'s own words, with transcript-holders named as the read that says which process is which.
  3. The lock had a session to rename and no pane to type into (suppressed, lanes-edit.sh:5536). The pane comes from the live record's tmux field and the harness has been seen to write NONE for a process plainly in a window (ruling (g)'s third fact) — a record record_is_here still places HERE by the pane's process tree. Both hooks run INSIDE the session's own pane, so the fallback is tmux display-message -p '#{pane_id}', asked of tmux rather than guessed, with M1's condition still asked of whatever it resolves to.
  4. M1's gate had no test (suppressed, lanes-edit.sh:5537) — "only while the pane's current command is claude" is the only barrier between this hook and somebody else's process. Both halves have one now: a pane running bash is not typed into and says so, and a pane whose command could not be READ is not typed into either.

Plus one line of documentation that was a promise the code does not keep: lane-end's usage offered --retire <pid|uuid> for both populations, and a duplicate cannot be named by a uuid — its uuid IS the row's own. The usage, the option block and the manual now say which form names which.

Declined, one line each

  • A per-workstation lock spanning the resume (lane-start:1807) — 18(h) asks for a COUNT, not a lock; every read in this toolset is a snapshot and the protocol answers the race at other layers (the append-only cell behind its anchor, and 18(b)'s ASK before a second place binds); a lock that has to be released at the exec it protects protects nothing, and serialisation at launch is an amendment rather than a review round.
  • (not asked) falls into the generic window arm (suppressed, lane-end:684) — that arm IS the documented intent and the behaviour asked for: (not asked) sets dr_own=1, which is what refusing to retire an unknown means (Amendment 18(b): a window that could not be asked about is unknown, not dead). The nit is the refusal's wording, reachable only with no tmux at all.
  • The literal-prefix fast path in under_projects_root (suppressed, lanes-edit.sh:5278) — for the prefix to match while $PROJECTS_ROOT is missing, a LIVE session's own cwd would have to exist under a parent that does not; and the resolution beneath it exists for the launcher's symlinked projects root, where erring toward "inside" is deliberate.
  • A uuid for a duplicate (suppressed, lane-end:26) — declined as code and taken as documentation, above.

Copilot round 3 — three taken, three declined, and the macOS job's own finding

Round 3 arrived 2026-09-14T22:51:22Z: 1 inline thread and 5 findings suppressed into the review body. Taken in 6519c1b, on a head that also carries three merges of main — b5244e2 (#44 and #46's pin), 66b4fb9 (#53), a954435 (#50) — and the fix for the three red lines tests-macos gave at 6cf5359.

THE macOS FAILURE WAS THE FIXTURE, NOT THE BEHAVIOUR. Round 2's case for the lock's pane fallback made the live record's pid a CHILD of the fake pane's, and pid_under climbs /proc to answer that — which macOS does not have. The record was "here" on Linux and nowhere on the macOS runner, so the guard refused with "no live session record in THIS window carries this session id" and the lock never ran. The fixture now gives the pane the record's OWN pid, which pid_under answers at its first step with no walk at all. Every other job was green at that commit; this is what the macOS job is for.

Taken

  1. pending — set by the session's first act is the other text a session cell can carry (suppressed, lanes-edit.sh:6038), and it is exactly what a yes to a fresh lane can leave: veto 1 refuses this window's uuid because the row being LEFT still records it. The guard's own last write anchored only on none recorded, so there it could not be made at all — the loop this branch fixed one commit earlier, reached through the one cell text the fix did not list. Both texts are anchors now, with a case driving it end to end.
  2. The lock typed into whatever pane the record named (suppressed, lanes-edit.sh:5475). here is true by the record's target OR by the pane's process tree, and tmux reuses window ids, so a record here BY ANCESTRY could carry another window's pane — and M1's gate cannot catch that, because that pane is running claude too. The pane now comes from the record only where its target names THIS window; otherwise guard_type asks tmux for the pane this hook runs in. ssb_name_line takes the same fence.
  3. --install's three planning refusals described a plan that no longer exists (suppressed, openRepoTools:760): no jq, invalid JSON, a non-object hooks. All three name both entries and print both blocks.

Declined

  • The companion should also require no tmux and no nameSource (lanes-edit.sh:5495) — it adds no detection (every duplicate this estate has met is a bg record with neither field) and it adds a way for a HARNESS RELEASE to block every prompt on a workstation, which this file's own window_session comment anticipates by name. kind is the field ruling (g) makes the discriminator.
  • session_files's roots are hard-coded (suppressed, lanes-edit.sh:5433) — that read predates this PR and is shared by live_holder, forks, window-session and who.
  • The destination log says RESUMED by <another id> after a move (suppressed, lanes-edit.sh:6106) — real, and filed as The offer's yes leaves the destination lane's log saying a transcript was RESUMED that nobody is in, because lane-start has no binding mode and the guard makes the cell write itself #70: the fix is a BINDING mode for lane-start, not a change to the guard. The cell every later read follows does end on the moved transcript.

Three merges of main, never a rebase. b5244e2 conflicted three times — --install's jq refusal (this branch named the pair; main added the regular-file check under it), its present arm (this branch split it per entry and made the write conditional; main gave it a chmod 600), and README.md's install paragraph with the cap that counts it (417 and 428 -> 436, both dated entries kept and a third naming the merge). 66b4fb9 and a954435 merged clean. Three counts main states in prose moved with the second hook entry, and the submodule is at the pin main brought.

Copilot round 4 — three taken, two declined, and the last round this lane takes

Round 4 arrived 2026-09-14T23:39:37Z: 2 inline threads and 3 findings suppressed. The reviews are not converging — 12 findings in round 1, 8 in round 2, 6 in round 3, 5 here — and what is left after this commit is either declined on the evidence above or filed as #59, #60 and #70.

Taken

  1. node is not claude, and M1 named one command — "only while the pane's current command is claude". The gate read claude|node on the reasoning that the harness is a node program and #{pane_current_command} reports the process rather than the wrapper. MEASURED on Eagle at 23:4xZ across the twelve panes tmux had: eleven lane panes report claude, two report bash (a session under a launcher wrapper), one zsh, and none reports node. The second word bought nothing on the estate it was written for and would have let a Node REPL in a pane take /rename <lane> and an Enter as input. A pane the gate refuses still gets the line printed for the person — and on this estate the bash panes are exactly the windows where it will be read.
  2. The SessionStart hook was undoing a person's own instruction before anybody was asked. Clause (f) types the rename for (h)1's DRIFT case, while (h) rule 2 makes a nameSource: user rename to another lane's name an INSTRUCTION answered yes/no at the next prompt — and this hook runs on every startup, resume, clear and fork, so a rename followed by a /clear had the name typed back before guard_run could put the question. It is said there now, in the words the guard will use, and left to the prompt that follows.
  3. The installer test module's docstring still described one merged SessionStart entry, seven tests into the pair's own section.

Declined

  • Classify the bg companion before the here branch (lanes-edit.sh:5535) — here and companion are the same answer to every caller (both are passed over in silence), so the order changes no outcome; what it would change is making this window's OWN companion a duplicate wherever the same-profile test does not hold, which is the false refusal round 2's narrowing exists to avoid. The duplicate this rule catches is never here.
  • record_window_state should tell a silent tmux from a refuted window (suppressed, lanes-edit.sh:5510) — (gone) and (not asked) already differ and lane-end protects the second; telling a dead SERVER from a dead WINDOW needs a second question put to tmux, which is a read this file makes nowhere yet and a change to who's liveness reading as much as to this one.

Copilot round 5 — filed, not taken, and this lane stops here

Round 5 (2026-09-15T00:05:57Z, 4 inline + 2 suppressed) is where the rounds stop paying: four of its six restate declines already answered on their own threads, and the two that are new are of the same class as them — a surface that PRINTS a wrong instruction in a rare state while writing nothing, killing nothing and letting no prompt through. All three of that class are filed together as #73: the silent-tmux read labelled (gone), lane-end --retire selecting the harness's companion when the act is typed OUTSIDE the lane's window (the companion verdict is relative to the asking process), and the (b) table's first cure printing --dir <path> <lane> for a lane named before the <repo>-<n> rule. Every thread on this PR is replied to and resolved.

The count of findings by round: 12, 8, 6, 5, 6 — 11 taken, the rest declined with evidence in the thread and, where they are real, filed as #59, #60, #70 and #73.

Closes #25
Closes #39

Lane: openrepotools-3 (openRepoTools-3)

🤖 Generated with Claude Code

Summary by Sourcery

Enforce lane identity and transcript ownership across prompts and lane lifecycle operations, and install the new guard alongside the existing session-start hook.

New Features:

  • Add a blocking UserPromptSubmit name guard that enforces agreement between the tmux window, session name, and register row, with automatic renames and interactive move offers.
  • Add shared transcript-holder detection to prevent multiple live processes from using the same transcript across lane guard, lane-start, and lane-end.

Bug Fixes:

  • Ensure hook failures fail closed for prompt submission while preserving SessionStart's non-blocking behavior.
  • Complete accepted lane moves by recording the current transcript in the destination lane and marking the source lane as moved.

Enhancements:

  • Add SessionStart reporting and rename behavior for session-name drift.
  • Make installation merge both hook entries atomically and idempotently while preserving user hooks.
  • Expand lane helper coverage for name mismatches, offers, duplicate holders, fail-closed reads, and hook behavior.

Documentation:

  • Document the name guard, lock, move offer workflow, duplicate transcript rules, fail-closed behavior, and installation changes.
  • Update installation artifact counts and clarify the guard's exit-code exception in project guidance.

Tests:

  • Add comprehensive shell tests for the name guard, lock, offers, duplicate transcript handling, and SessionStart behavior.
  • Add installer tests covering the UserPromptSubmit hook shape, conflicts, idempotence, and all-or-nothing settings updates.

brettheap and others added 7 commits September 14, 2026 19:29
…k, the offer, `transcript-holders` and the second hook entry, as the session that was writing them left them — unverified, no suite run behind it

THIS COMMIT PROVES NOTHING AND IS NOT A CLAIM THAT ANY OF IT WORKS. The
session building Amendment 12 was ended mid-work and left five files modified
in this worktree with nothing committed; this is those bytes, recorded before
anything else is done to them, so that the review that follows has a base to
diff against and so that a second interruption cannot cost the work twice. No
`bash tests/test_lane_helpers.sh` and no `python3 -m pytest` has been run
against it by the session making this commit. `bash -n` is clean on all five,
which is the whole of the evidence: `lanes-edit.sh`, `lane-start`, `lane-end`,
`openRepoTools`, `tests/test_lane_helpers.sh`.

WHAT IS IN IT, by file and by act of the brief:

* `lanes-edit.sh` (+927): the `guard` verb and `guard_run`'s (b) table — the
  scope exemptions (`agent_id`, a `cwd` outside `$PROJECTS_ROOT`), the triple
  printed once, the eight rows with one filled-in cure each, `guard_lock_rename`
  typing `/rename <lane>` into this pane, `guard_offer`/`guard_answer` under
  `$CLAUDE_CONFIG_DIR/lanes/offers/<uuid>` with yes/no/anything-else, and
  Amendment 18(h)'s refusal. `transcript_holders` (act 1b's read) with its
  four-valued verdict, `record_window_state` (Amendment 11(h)'s agreement
  rule), `lane_shaped`, `under_projects_root`, `rows_named_ci`,
  `lane_binding_utc`, `guard_rename_is_newer`, `guard_type`, and clause (f)'s
  `ssb_name_line` hung off `session_start_block`.
* `lane-start` (+70): `dup_check`, called at the window-binding step and inside
  the row-resume branch.
* `lane-end` (+148): `--retire <pid>` taking a duplicate of the row's OWN id,
  with the window's own session refused by name.
* `openRepoTools` (+132): the `UserPromptSubmit` entry beside the `SessionStart`
  one, both merges staged before either is placed.
* `tests/test_lane_helpers.sh` (+326): the `== Amendment 12 ==` section and the
  fake tmux's `send-keys` log, `%pane` targeting, `#{session_name}`,
  `#{pane_current_command}` and the opt-in sticky `rename-window`.

WHAT IS NOT IN IT, and is the work after this commit: no run of either suite;
act 2 (`lane-start --name` on every launch branch) unverified; the pytest side
of act 5 (the installer's second entry, the artifact count 18 → 19, the README
caps) absent — `grep -c eighteen` still answers for eighteen artifacts; act 6's
`docs/README-lanes.md` section absent; Amendment 15's `canon_lane` not yet on
`main`, so `rows_named_ci` here is a second spelling of that round's function
and the two have to become one when #41 lands; and Amendment 18 Addendum 2
(the `restart` COMMAND becomes `lane <name>`) is not reflected in any message.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dment 12 section and every `lane-start` window binding with it is fixed, and the artifact count, the docs and the pytest half of act 5 are written — one shell run behind them, the next not yet made

WIP BECAUSE NO SUITE HAS RUN SINCE THE FIX. The run that FOUND it is measured
below; the run that proves it is not yet made, and neither is any `pytest`.

THE DEFECT, and it was one line. `transcript_holders` never initialised
`th_here_prof`, and `lanes-edit.sh` runs under `set -u` (line 292): pass 2's
`[ -z "$th_here_prof" ]` is reached for the FIRST record that is not this
window's, and an unset variable there exits the shell with `unbound variable`.
Measured on the predecessor's state at `3befe7c`, `bash tests/test_lane_helpers.sh`:
**1468 passed, 124 failed** — 73 of the failures the WHOLE of the new
`== Amendment 12 ==` section, each one stderr reading
`lanes-edit.sh: line 4961: th_here_prof: unbound variable`, and the rest every
`lane-start` case that reaches step 3b's window binding, because `dup_check`
reads that 1 as "the records could not be read" and refuses fail-closed
(`the same lane live in THIS window is not a collision`, `lane-start does NOT
refuse the window its own session is running in`, all four `veto` cases, …).
`th_here`, `th_prof`, `th_wrc` and `th_final` are initialised beside it.

THE COUNT, EVERYWHERE IT IS DERIVED OR STATED (act 3's second half): 18 -> 19,
because `--install` now merges TWO entries into one `settings.json`.
`tests/test_openrepotools_command.py` derives it — `ARTIFACTS = len(INSTALLED)
+ 2*len(SKILL_NAMES) + 2*len(COMMAND_NAMES) + HOOK_ENTRIES`, the `+ 1` that was
hand-kept now a named `HOOK_ENTRIES = 2` — and `openRepoTools` states it in six
places, README.md in two, `docs/README-lanes.md` in one. "THE SEVEN ARTIFACTS
THAT ARE NOT ONE OF THE ELEVEN FILES" is eight.

DOCS (act 6): `docs/README-lanes.md` gains "The name guard and the lock
(Amendment 12)" before "Hand edits" — the triple, the (b) table filled in, the
lock and its pane conditions, the offer and its three answers, Amendment 18(h)'s
duplicate read and its three callers, clause (f)'s line, fail-closed with the
one bypass, and the scope. README.md's install paragraph names the second entry,
what it does, and that a person's own `UserPromptSubmit` hooks are left where
they are; its cap moves 409 -> 417 with a dated entry that says what the eight
lines bought.

PYTEST (act 5's other half): seven cases in
`tests/test_install_skill_and_hook.py` — the entry byte-identical and with NO
`matcher`, the `|| true` that must NOT be on it asserted on its own, twice adds
one entry, a differing `guard` entry refuses and places nothing,
`~/projects/xFactory/guard.sh` is NOT a conflict (the verb collides, not the
file — R-A9-14 one event over, and this estate's usage guard is exactly that
string), the pair is ONE all-or-nothing merge, and one write places both.

ALSO: the comment that resolved `lane-start` "the way `restart` finds
`lanes-edit.sh`" no longer names a file Amendment 18 Addendum 2 is removing
(#43); it names this file's siblings.

WHAT IS LEFT: the shell suite re-run and a `pytest`; hardening the two hooks
against a fatal inside the read (this crash exited `session-start` 1, against
R-A8-1's "always exits 0", and would exit `guard` 1, which lets a prompt
THROUGH — the opposite of clause (d)); act 2 verified done and to be reported;
`git merge origin/main` once #41 lands.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nst a fatal inside their own read, and AGENTS.md no longer promises exit 1 for the one command that must answer 2

WIP BECAUSE THE SUITE RUN BEHIND IT WAS STOPPED at 431 lines, 0 failures, to
merge `origin/main` under it — bash reads a script as it executes, and
Amendment 15's branch rewrites `tests/test_lane_helpers.sh`.

THE HARDENING, AND IT IS THE `th_here_prof` CRASH READ FOR WHAT IT ACTUALLY
BROKE rather than only fixed. That crash was an unbound variable under `set -u`,
which exits the shell where it stands — so `|| :` and `|| rc=$?` at the call
site caught nothing — and the two hooks failed in the two directions neither is
allowed to fail in:

  * `session-start` exited **1** and printed no block at all, against R-A8-1's
    "this hook never fails and always exits 0". Measured: `the SessionStart hook
    still exits 0 with the name wrong — it never refuses` → `expected [0], got
    [1]`.
  * `guard` would exit **1**, and a 1 does not block a `UserPromptSubmit` —
    only a 2 does. A fatal inside the guard let the prompt THROUGH, in silence,
    which is the one failure Amendment 12(d) forbids by name and the exact state
    the whole amendment exists to end.

So the `guard` dispatcher arm runs `guard_run` in a SUBSHELL and maps every
code but 0 and 2 to a 2, naming the failure and the one bypass; `ssb_name_line`
runs `transcript_holders` in one, and `session_start_block` calls
`ssb_name_line` in one. The effects that must cross a subshell all do — the
offer file, `lane-start`, the register write, stderr — and the only thing lost
is shell state nothing reads afterwards. This is clause (d) made true of the
guard's OWN failures and not only of the reads it makes.

AGENTS.md (act 6's second half): its lane paragraph promised, without
qualification, that each of the four "refuses with exit 1 naming `openRepoTools
wip init` where that file does not answer". `lanes-edit.sh guard` deliberately
answers that same unreadable workspace with 2, for the reason above — so the
sentence now carries the exception and says why it is a mechanism and not a
taste. Four lines; the cap moves 224 -> 228 with a dated entry.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dment 12 branch — four conflicts in `lanes-edit.sh` and one in the suite, every one of them the two rounds writing in the same place about the same rule

MERGE AND NEVER A REBASE: this branch is pushed, and its two WIP commits are
the record of a session that was killed mid-work.

THE FIVE CONFLICTS, and each resolution keeps BOTH rounds rather than picking:

1. `lanes-edit.sh`'s EXIT CODE TABLE, row 2. Both rounds added a meaning to
   the same number and both wrote "FIVE MEANINGS ON ONE NUMBER" over a list
   that had four. The resolution carries both — Amendment 15's two rows
   differing only by case, and Amendment 12's BLOCKED PROMPT — and the count
   is SIX, with the paragraph below it renumbered from "THE FIFTH" to "THE
   SIXTH". A table that says it is complete and is not is worse than no table.

2 & 3. `rows_named_ci`. The two rounds wrote the SAME FUNCTION, body for body —
   this branch spelled it that way on purpose, saying so in the comment it
   carried ("so the two collapse into one when it lands rather than becoming
   two readings of one rule"), and they collapsed. Amendment 15's header stays,
   because it is the one that states the RULE and `canon_lane` sits under it;
   this round's comment becomes three sentences inside it saying why a third
   caller asks the same parse: the guard needs the COUNT and has to print its
   triple before it refuses, so it cannot take `canon_lane`'s own `note` + 2.
   One parse, three readings, no second rule about what a lane name is.

4. The `*)` arm's subcommand list: both `guard`/`transcript-holders` and
   `canon-lane`, which `test_the_unknown_subcommand_refusal_names_every_subcommand_there_is`
   derives from the dispatcher's own arms and would have failed on either alone.

5. `tests/test_lane_helpers.sh`: both rounds appended a section immediately
   before `== the workstation seam ==`. Both are kept, Amendment 12's first and
   Amendment 15's second, which is the order #25's brief asks for and leaves the
   seam second-to-last as it has always been.

AND ONE THING THE MERGE MADE NECESSARY THAT NO CONFLICT SHOWED. The Amendment
12 section now sits BETWEEN two others instead of at the end, and it had been
tearing its fixture DOWN — `unset FAKE_TMUX_WINDOWS FAKE_TMUX_WINDOW_NAME`,
`export FAKE_TMUX_WINDOW=testsess:@1` — rather than putting it back. That was
harmless while nothing followed it and is a section changing the premise of
every case after it from a distance now that Amendment 15's does. The three
variables are CAPTURED at the top of the section and restored at the bottom,
each with its own `if [ -n … ]` so that "was unset" comes back unset, which is
the state `main` ran Amendment 15's cases in.

`restart` moved under the merge; it is `main`'s change (#41) and this branch
does not touch that file — Amendment 18 Addendum 2 removes it in #43.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… plug in and WHY they cannot be built yet, and three sentences this round left saying the opposite of what is now true

FOUR CORRECTIONS, none of them behaviour, and every one of them a sentence a
reader would act on.

1. AMENDMENT 18 (d) AND (e), the one thing #25's brief asks for that the
   predecessor's state did not carry. A comment block inside `guard_run`, at the
   point both reads belong — after (h)'s count, with the lane known and nothing
   yet judged — saying what each does and why neither is code here: (d)'s answer
   IS Amendment 17(a)'s handoff under `--exit` (#36) and
   (e)'s `PAUSED … on behalf of` is written by the `--force` writer Amendment 18
   adoption act 1 adds (#38). A guard refusing on a line no writer in this
   estate can yet produce would be refusing on a read of a log that never
   carries it, and one that TYPED `/handoff --exit` into a pane where that flag
   does not exist would type a command that does nothing into somebody's
   session.

2. `transcript_holders`'s verdict table said the `here` test was
   "`record_is_here`'s tiers 1 and 2; its tier 3 cannot discriminate". It is
   tiers 2 AND 3, and it is TIER 1 that cannot: tier 1 asks whether the
   record's `sessionId` is `$CLAUDE_CODE_SESSION_ID`, and every candidate in
   this set carries the very id being tested — so on the asking session's own
   transcript it would answer `here` for the DUPLICATE too, which is the one
   reading this function exists to prevent. The code was always right; the
   comment named the wrong two.

3. `session_start_block` carried, from Amendment 15's round, "AMENDMENT 12'S
   GUARD AND LOCK PLUG IN HERE (#25, NOT BUILT)". They
   are built, and the line immediately below the comment is clause (f).

4. `openRepoTools --help` and the install paragraph beneath it named "the
   SessionStart hook entry" and "ONE `SessionStart` entry" — the two places a
   person reads before running the command, and the only two left saying one
   where the count above them says two. Both name the `UserPromptSubmit` name
   guard, what it refuses, and that every `UserPromptSubmit` hook of the
   person's own is left exactly where it is.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…E line, as its neighbour does — the phrase is the contract and a line break through the middle of it is a phrase no reader and no test can find

FOUND BY THE TEST WRITTEN FOR THE BEHAVIOUR, which is the whole argument for
writing it: `test_a_differing_guard_entry_refuses_and_places_nothing` asserted
the same four things its `SessionStart` twin asserts and failed on the fourth —
`assert 'NOTHING was installed' in '\nREFUSED: … not an installer's to do.
NOTHING was\n    installed. Remove that entry…'`. The refusal DID say it; it
said it across a line break, so the sentence a person scans for and the string
the suite looks for were both absent while the text read correctly to a human
reading it slowly.

THE PHRASE IS THE CONTRACT AND NOT PROSE. `--install` is all-or-nothing, and
"NOTHING was installed" is the sentence that tells a person the bin directory,
the skills, the command file and the OTHER hook entry are all untouched — it
appears on ten other refusals in this file, every one of them on a single line.
The paragraph is rewrapped around it rather than the phrase squeezed in: the
"two refusals for one prompt" clause ends its own sentence, and the reason and
the consequence go together on the next line.

`python3 -m pytest tests/test_install_skill_and_hook.py -q`: 35 passed, 0
failed (28 before this round, 35 with Amendment 12's seven).

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e `lane-start` may not — its veto 1 never moves a uuid another row records, and without that last write the guard refuses for ever on a state it created by obeying the person

THE DEFECT, MEASURED, AND IT IS A LOOP. Clause (h) rule 2 names the end state
and rule 3 repeats it: after a `yes`, *"window X, session X, ROW X STAMPED WITH
THIS UUID"*. `lane-start --no-launch` does every other part of it and CANNOT do
this one — its step 3b VETO 1 (Amendment 11 clause (d) rule 1) never takes a
uuid that belongs to ANOTHER ROW, and after the rename this uuid still belongs
to the lane being left. So it mints a fresh id instead. In the suite: `yes`
moved the window to `repoGD-2`, whose row then read `STARTED by 7a01ae69…`
while the session was `aaaa0012-1111…`. The NEXT prompt therefore finds a lane
window whose row does not name this transcript — the last row of the (b) table
— and refuses with `lane-start --no-launch repoGD 2`, which vetoes for the same
reason and changes nothing. A blocking hook that refuses for ever, on a state
it created by obeying the person, is the worst outcome this surface has.

THE VETO IS RIGHT AND STAYS. It exists for the take nobody asked for —
Evidence 2(b), *"`lane-start openXfactory-5` typed from a window named
`openRepoProject-1`"* — and what clause (h) rule 4 forbids is moving a uuid
between rows WITHOUT the person's `yes`. Here there is one, on the record,
answered at this very prompt. So `guard_bind_uuid` makes that one write after
`lane-start` returns, with `lane-start`'s OWN anchor discipline unvaried: the
anchor is the PUBLISHED last id, so a checkout whose copy of the row is older
than the one that landed refuses rather than writing a cell that no longer
matches; a cell recording no uuid at all is anchored on `none recorded`, the one
other text this act can meet; anything else prints the act and guesses at
nothing. Both cached copies of the register are dropped first — the variable and
the file every subshell reads — because this is the one read in the file that
happens AFTER a writer has moved the ref, and a row `lane-start` has just
CREATED is not in the cache at all.

AND TWO FIXTURES IN THE NEW SECTION THAT DESCRIBED A WORLD THAT NO LONGER
EXISTS, both of which made real assertions pass over the behaviour they name:

  * `$G_OUT` IS KILLED hundreds of cases above this section (the liveness
    section's own teardown), so every Amendment 18(h) case here was writing a
    record for a DEAD pid — which `record_is_live` correctly rejects, so the
    guard exited 0, `lane-start` bound the transcript, and `lane-end --retire`
    answered 8. Twelve of the twenty failures were that one line. The section
    starts a live process of its OWN and kills it at its end, and the suite's
    `cleanup` trap takes it too.
  * `add-row` HAS REFUSED TWO ROWS FOR ONE NAME since Amendment 15 landed
    (#41), which is the point of the guard's own row for that state: the pair
    can no longer be created by the tooling, only met. So it is seeded the way
    it actually arrives — straight into the register and pushed — the same
    `add_seed_row` + commit the Amendment 15 section uses.

TWO REGRESSION TESTS FOR THE TWO CONTRACTS THE `set -u` CRASH BROKE, each
holding the pair of hooks to its OPPOSITE rule against ONE fixture: the same
unreadable session records refuse the prompt (clause (d)) and still leave
`session-start` exiting 0 with its whole block printed, down to the `(no fetch)`
tail every branch ends with (R-A8-1); and a register this workstation cannot
read blocks the prompt with a 2, never the dispatcher's 1 — the exception
AGENTS.md now carries, and the one number that blocks a prompt at all.

Docs: the manual's offer section states the last write and why `lane-start` may
not make it.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 14, 2026 20:38

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 3 days and 6 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Sep 14, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR implements Amendment 12's blocking lane name guard and rename/offer workflow, adds shared Amendment 18(h) duplicate-transcript enforcement across guard, lane-start, and lane-end, installs the guard atomically with the existing SessionStart hook, hardens hook failure handling, and adds comprehensive tests and documentation.

Sequence diagram for the lane name guard prompt check

sequenceDiagram
    participant Claude
    participant Guard as lanes-edit.sh guard
    participant Tmux
    participant Records as Session records
    participant Register as Lane register

    Claude->>Guard: UserPromptSubmit payload
    Guard->>Tmux: Read window and pane state
    Guard->>Records: transcript-holders(session_id)
    Guard->>Register: Read lane row
    alt Outside projects root or agent prompt
        Guard-->>Claude: Exit 0 silently
    else Three names agree
        Guard-->>Claude: Exit 0
    else Mismatch or indeterminate read
        Guard-->>Claude: Print refusal and cure
        Guard-->>Claude: Exit 2, prompt blocked
    end
Loading

Sequence diagram for automatic lane rename and user rename offer

sequenceDiagram
    participant User
    participant Guard as lanes-edit.sh guard
    participant Tmux
    participant LaneStart as lane-start
    participant Register

    alt Session name drifted from bound lane
        Guard->>Tmux: Read pane_current_command
        Guard->>Tmux: send-keys /rename lane Enter
        Guard-->>User: Refuse this prompt, send again
    else User renamed session to another lane
        Guard->>Guard: Write pending offer
        Guard-->>User: Ask yes or no, block prompt
        User->>Guard: Next prompt answer
        alt yes
            Guard->>Tmux: rename-window target lane
            Guard->>LaneStart: lane-start --no-launch repo n
            Guard->>Register: Append current uuid to target row
            Guard->>Register: Write MOVED → target lane
        else no
            Guard->>Tmux: send-keys /rename old lane Enter
        else Other answer
            Guard-->>User: Re-offer, block prompt
        end
    end
Loading

File-Level Changes

Change Details Files
Add a fail-closed UserPromptSubmit name guard that enforces lane identity across tmux window, live session name, and register row.
  • Parse hook payloads and apply project-root, subagent, tmux, register, row-position, and case-canonicalization rules.
  • Return exit 2 for mismatches or indeterminate reads, with the observed triple and a filled-in remediation.
  • Type /rename <lane> into the pane only when the pane command is eligible; otherwise provide a manual cure.
  • Persist and consume person-initiated rename offers, handling yes/no answers and recording an approved move.
  • Bind the current transcript UUID into the destination lane after an approved move and mark the source lane as moved.
lanes-edit.sh
tests/test_lane_helpers.sh
docs/README-lanes.md
README.md
AGENTS.md
Centralize detection of duplicate live processes holding the same transcript and enforce one-holder semantics across lane operations.
  • Sweep session records across all profiles and classify holders as here, companion, duplicate, or unrelated while validating tmux window identity by id and session name.
  • Make the prompt guard refuse duplicate holders and name the appropriate retirement command.
  • Make lane-start refuse duplicate binds/resumes and lane-end retire qualifying duplicates while rejecting retirement of the lane's own session.
  • Harden hook callers against unreadable records and set -u failures without violating SessionStart's always-zero contract.
lanes-edit.sh
lane-start
lane-end
tests/test_lane_helpers.sh
docs/README-lanes.md
Install the new guard hook alongside SessionStart with pair-level atomic, idempotent settings merging.
  • Add the exact UserPromptSubmit command with a five-second timeout and no matcher or `
Extend SessionStart diagnostics to repair session-name drift while remaining non-blocking.
  • Reuse the duplicate-holder read and report competing live processes.
  • Type the canonical /rename <lane> when possible and emit the clause (f) rename diagnostic.
  • Contain failures so SessionStart continues to exit 0.
lanes-edit.sh
tests/test_lane_helpers.sh
docs/README-lanes.md
Expand coverage and update protocol-facing documentation for the new lane identity and transcript ownership rules.
  • Add end-to-end shell fixtures for every guard state, lock behavior, offers, fail-closed reads, duplicate holders, retirement, and SessionStart behavior.
  • Add installer tests for hook shape, conflict handling, idempotency, boundary matching, atomicity, and permissions.
  • Update README, lane documentation, AGENTS guidance, and hygiene/count assertions.
tests/test_lane_helpers.sh
tests/test_install_skill_and_hook.py
tests/test_openrepotools_command.py
tests/test_repo_hygiene.py
README.md
docs/README-lanes.md
AGENTS.md

Assessment against linked issues

Issue Objective Addressed Explanation
#39 Extend lane-end <lane> --retire <pid> to retire a live duplicate process whose session ID is the lane row's current session ID, while refusing to retire the lane window's own live session. ✅
#39 Make lane-start detect and refuse binding or resuming a transcript that is already held by multiple live processes, including duplicates found across profiles, rather than appending or launching another holder. ✅
#39 Have the relevant tooling identify and communicate the duplicate process and the appropriate lane-end --retire <pid> action, with tests covering one window holder, one background holder, and the same-session-ID collision. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate findings remain in the guard, lifecycle commands, and hook installer.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR enforces lane identity across tmux sessions and register rows, prevents duplicate transcript holders, and installs the related hooks.

Changes:

  • Adds prompt guarding, rename locking, move offers, and SessionStart reporting.
  • Adds duplicate-holder checks to lane lifecycle commands.
  • Updates hook installation, tests, documentation, and artifact accounting.
File summaries
File Summary
tests/test_repo_hygiene.py Reviewed hygiene checks.
tests/test_openrepotools_command.py Reviewed installer artifact accounting.
tests/test_lane_helpers.sh Reviewed guard, holder, offer, and SessionStart coverage.
tests/test_install_skill_and_hook.py Reviewed hook installation and conflict tests.
README.md Reviewed installation documentation updates.
openRepoTools Reviewed atomic hook installation; unresolved findings remain.
lanes-edit.sh Reviewed guard, offers, holder detection, and binding; unresolved findings remain.
lane-start Reviewed duplicate-holder validation; unresolved findings remain.
lane-end Reviewed duplicate transcript retirement; unresolved findings remain.
docs/README-lanes.md Reviewed lane protocol documentation.
AGENTS.md Reviewed guard exit-code guidance.
Review details

Suppressed comments (5)

lanes-edit.sh:6023

  • lane_start_args emits a literal <path> placeholder for lane names that do not match <repo>-<n>; that formatter is suitable for a diagnostic, not an executable argument list. Accepting an offer to an existing lane such as browser-ui-repair therefore invokes lane-start --dir <path> browser-ui-repair, fails to move, and leaves the offer retrying forever. Resolve the destination checkout directory (or ask the person for it) before invoking lane-start.
    # UNQUOTED ON PURPOSE: `lane_start_args` prints TWO words, `<repo> <n>` —
    # or `--dir <path> <lane>` for a lane named before the `<repo>-<n>` rule.
    # shellcheck disable=SC2046
    "$ga_start" --no-launch $(lane_start_args "$ga_to") >&2 || ga_rc=$?

lanes-edit.sh:5758

  • The pending-offer branch runs before the Amendment 18 duplicate check. If another process starts carrying this transcript after the offer is written, the next yes/no is consumed and may rename or run lane-start instead of refusing every prompt for the duplicate; yes can also proceed into the move path. Check gr_others before consuming an offer and retain the offer while the duplicate is unresolved.
  # ---- THE PENDING OFFER, ANSWERED BEFORE ANYTHING ELSE IS JUDGED (clause
  # (h) rule 2). The answer prompt is the person's reply to a question this
  # guard asked, it is consumed here and never reaches the model, and the
  # triple still disagrees BECAUSE of the rename being answered — so this
  # cannot wait behind the table below.
  gr_off="$(guard_offer_file "$G_ID")"
  if [ -f "$gr_off" ]; then
    guard_answer "$gr_off" "$gr_prompt" "$gr_pane"
    return 2

lanes-edit.sh:5422

  • A matched session record is read with cat ... || :, so a record that disappears or becomes unreadable after the grep pass is silently skipped and transcript_holders can return 8 as if no holder existed. lane-start then permits the bind/resume despite not completing the one-live-process read. Propagate this read failure as an indeterminate result instead of treating it as an absent record.
  while IFS= read -r th_f; do
    [ -n "$th_f" ] || continue
    th_blob="$(cat -- "$th_f" 2>/dev/null || :)"
    [ -n "$th_blob" ] || continue

lanes-edit.sh:5632

  • under_projects_root returns the same nonzero status for a proven outside path and for an indeterminate failure to resolve the cwd or projects root. This || return 0 treats both as the D1 exemption, so a failed symlink/root or cwd read can silently let a prompt through even though Amendment 12(d) requires fail-closed behavior. Return a distinct indeterminate status and turn it into guard_refuse/exit 2; only a proven outside result should be silent.
  under_projects_root "$gr_cwd" "${PROJECTS_ROOT:-$HOME/projects}" || return 0

openRepoTools:726

  • The guard conflict check has the same ordering bug: if the exact guard entry and a different guard entry coexist, index($c) returns present before the rival is examined, so installation succeeds with two prompt guards. Detect a differing guard command before treating the exact entry as idempotent.
	existing_guard="$(jq -r --arg c "$GUARD_COMMAND" '
		[ (.hooks.UserPromptSubmit // [])[]? | (.hooks // [])[]? | .command // empty ] as $cmds
		| if ($cmds | index($c)) then "present"
		  elif ($cmds | map(select(test("(^|\\s)guard(\\s|$)"))) | length) > 0 then "conflict"
		  else "absent" end' "$settings" 2>/dev/null || printf 'error')"
  • Files reviewed: 11/11 changed files
  • Comments generated: 7
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lane-end
Comment thread lane-start Outdated
Comment thread lanes-edit.sh Outdated
Comment thread openRepoTools
Comment thread lanes-edit.sh
Comment thread lanes-edit.sh
Comment thread lanes-edit.sh
…see — the holder of an id THIS WINDOW has no record of, and a second INTERACTIVE session in this profile — and the offer stops answering itself while one is live or running a `lane-start` it could not fill in

COPILOT ROUND 1 ON #52: seven inline threads and five findings suppressed into
the review body. FOUR ARE REAL AND ARE HERE; the other eight are declined on
the PR with the evidence, one line each. Two of the four are checks that could
not fire at all, which is the worst shape a fence has.

1. `lane-start`'s ROW-RESUME BRANCH COULD NEVER REFUSE, and it is the branch
   Amendment 18(h) names first: *"the id it is about to RESUME or bind"*. That
   call is reached only when `row_sid != win_sid` — so this window carries no
   record of the id being resumed, so `transcript_holders` answers `unrelated`
   for every live holder of it, and `dup_check` counted only `duplicate`. A
   `--fork-session` in a background pty host holding the row's own transcript
   was invisible to the one read written to see it, and `lane-start` would have
   launched the second resume that #39 was filed for.

   `unrelated` now blocks IN `dup_check` and nowhere else, because the question
   differs from the one the shared read answers: `transcript_holders` keeps
   `unrelated` silent so that a rule about a transcript cannot overturn
   Amendment 8(f)'s orphan (reported, never refused) or `forks`, both of which
   ask about a LANE. `dup_check` asks 18(h)'s own question about ONE id. At the
   window-binding call that id IS this window's session, so a `here` record
   exists and no verdict is `unrelated` — one rule, two call sites, no second
   definition of a duplicate.

2. THE COMPANION IS THE `bg` RECORD, NOT ANY RECORD IN THIS PROFILE. Amendment
   8 ruling (g) names exactly one thing to pass over in silence: *"Beside the
   interactive process in the pane the harness runs a companion: `kind: bg`, no
   `tmux`, no `nameSource`, its own pid, and THE SAME `sessionId`"*. Keyed on
   the profile alone, a second live INTERACTIVE record beside this window's own
   read as that companion — and one profile resuming one transcript twice is
   the CHEAPEST way there is to make two live processes on one file: no move,
   no swap, one `--resume`. `record_fields_are_session` is this file's own
   predicate for the distinction and is asked here rather than re-spelled, so
   `bg` is a companion and `interactive` (or a harness too old to say) is a
   holder, in one reading for all three surfaces.

3. AN ANSWER IS A PROMPT. Clause (h) refuses *"every prompt in a process that
   shares its session id with another live one"*, and the offer's `yes`/`no`
   was being consumed in front of that count: a `yes` runs `lane-start`, writes
   a row and appends a uuid to a cell — out of a process that may not be
   writing at all, on an offer file the OTHER process could equally have
   answered. 18(h)'s refusal now stands in front of the answer and the offer is
   KEPT, to be answered at the first prompt after the duplicate is retired.

4. THE OFFER NEVER RUNS A COMMAND IT COULD NOT FILL IN. `lane_start_args`
   answers `--dir <path> <lane>` for a lane named before Rule 4's `<repo>-<n>`
   form — the register carries 22 of them — and that `<path>` is a PLACEHOLDER:
   nothing in the register, the window or the session says where such a lane's
   checkout is. Printing it in a diagnostic is honest; RUNNING it is not, and a
   `yes` did. `lane-start --no-launch --dir '<path>' legacy-ui` refuses on a
   directory that does not exist, and the offer would then ask the same question
   at every prompt afterwards. The offer now says what it cannot fill in, `yes`
   refuses with the act and the one word left to the person, and the offer is
   kept so `no` still answers it.

AND THE TWO ROWS OF THE (b) TABLE THAT HAD NO CASE, found by reading the table
against the section rather than by the review: **not inside tmux at all**
(ratified decision D2) and a **tmux that answers nothing**, which is clause
(d)'s indeterminate read and a different state with a different message. The
first is the one cure in the table that cannot be filled in — which lane a
windowless session belongs to is exactly what nothing there knows.

TESTS, all in the Amendment 12 section: a same-profile INTERACTIVE twin refused
by pid, window and profile, with the harness's own `bg` companion beside it
still exiting 0 in silence, so the verdict is proved to turn on the KIND and not
on the directory; a row whose cell records an id with a transcript here and a
live `bg` holder of it in another profile, `lane-start repoGD 3 --no-launch`
refusing 1 and launching nothing, then resuming it once that process is gone; a
pending offer NOT answered while a duplicate is live, and still pending
afterwards; a lane named `legacy-ui` offered, its `yes` refused with
`--dir <that lane's checkout>` and the offer kept, its `no` answering; `TMUX=`
refused with the window half empty and nothing typed; a `tmux` that exits 1
refused as a read that did not happen, with the `--safe-mode` bypass named.

Docs: the manual's offer section carries the wait behind the duplicate read and
the lane it cannot fill in; its duplicate-read section says what a companion is
and that the kind and not the profile is the discriminator.

AND TWO ASSERTIONS CHANGED, both with the citation and neither weakened.
Amendment 8's `lane-start does not refuse a lane over an orphan` and `…so it
cannot refuse a lane either` (a lone `bg` companion) now expect **1**, because
the run they make next is `claude --resume <the row's id>` and a live process
already carries that file. Everything else those two cases assert is untouched
and still green: the orphan is REPORTED, named *"orphaned holder, not a rival"*,
given 6(d)'s act, and the rival refusal (`take repoGH-2`) is still absent — the
LANE is not taken by it; what is refused is the second resume of its
TRANSCRIPT, which is 18(h)'s own sentence. Amendment 8(f)'s rule does not reach
that state either: it governs *"a row's EARLIER session ids … still held by live
but idle sessions"*, and this is the id the row ENDS on. Two `has` lines are
added to each, so the refusal they now take is asserted rather than assumed.

COUNTS. `bash tests/test_lane_helpers.sh`: **1777 passed, 0 failed** at
`109186b` -> **1815 passed, 0 failed** here (38 assertions: 8 for the two (b)
rows, 26 for round 1's four, 4 on the two cases above). `python3 -m pytest
tests -q` is queued behind three other writers' runs on this workstation's one
lock; CI runs the same suite on four jobs, and the round before this one was
578 passed, 0 failed.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m
@brettheap

Copy link
Copy Markdown
Contributor Author

Copilot round 1 is answered at eedb55f: four taken, eight declined, each thread replied to and resolved, and the body's new "Copilot round 1" section carries every one with its evidence in a line.

The five findings Copilot suppressed into its review body have no threads to reply in, so they are answered there too: two of them are taken (the offer's answer was consumed in front of Amendment 18(h)'s refusal; the offer would have RUN lane-start --dir <path> <lane> with a literal placeholder for a lane named before the <repo>-<n> rule), and three are declined — a record that becomes unreadable mid-pass is already fail-closed one line above it (grep -l … 2>"$th_err" and any stderr at all returns 1; what the cat … || : passes over is a file that VANISHED between two reads, which is a session that ended); under_projects_root's indeterminate answer is clause (e)'s SCOPE and not the triple, and this hook runs at every prompt of every session on the workstation, so refusing the ones we cannot prove are ours is the larger failure; and the guard-entry conflict ordering is the same finding as the SessionStart one, filed as #59.

Two assertions moved, both cited in the body and neither weakened: Amendment 8's orphan and lone-bg-companion cases now expect lane-start to exit 1, because the act it makes next is claude --resume <the row's id> and a live process already carries that file (Amendment 18(h), "never launches a second resume of it"). Everything else those cases assert is still green — the orphan is still reported, still "not a rival", still given 6(d)'s act, and the rival refusal is still absent.

bash tests/test_lane_helpers.sh: 1777 → 1815 passed, 0 failed.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate findings remain in duplicate-process enforcement, retirement handling, and guard behavior.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (5)

lane-end:26

  • The new usage line advertises a UUID as valid for retiring a duplicate, but the duplicate branch only matches retire_target against dr_pid (lines 688-691). Passing the row's UUID therefore falls through to the generic “neither” refusal even when a live duplicate exists. Either implement an unambiguous UUID-to-duplicate selection or document UUIDs only for forks and require a PID for duplicates.
#   lane-end <lane> --retire <pid|uuid>  # retire a live FORK or DUPLICATE; ends nothing

lane-end:684

  • The (not asked) state is supposed to protect the process, but this wildcard also matches window ... (not asked). When tmux cannot be queried or the target has no usable window id, the target is therefore added to dup_own and --retire refuses it as the lane's own session instead of preserving the unknown state. Match (not asked) explicitly before the generic window case so only a resolved window is treated as the owner.
          case "$dr_where" in
            "window "*"(gone)") dr_own=0 ;;
            "window "*) dr_own=1 ;;
            *) dr_own=0 ;;
          esac

lanes-edit.sh:5278

  • The literal-prefix fast path returns before resolving the root, so a missing or unreadable $PROJECTS_ROOT still classifies any matching cwd string as in scope. For example, with root /tmp/projects removed and cwd /tmp/projects/repo, the guard proceeds instead of treating the session as outside the projects root, contrary to the scope contract documented above. Resolve the root first, then apply the textual-prefix shortcut.
  case "$upr_c/" in "$upr_r"/*) return 0 ;; esac
  upr_cr="$(cd -- "$upr_c" 2>/dev/null && pwd -P)" || upr_cr=""
  upr_rr="$(cd -- "$upr_r" 2>/dev/null && pwd -P)" || upr_rr=""
  [ -n "$upr_cr" ] && [ -n "$upr_rr" ] || return 1

lanes-edit.sh:5536

  • Records without a tmux field are explicitly supported by transcript_holders: a current record can be classified here from its pid, but the callers then leave gr_pane/snl_pane empty because they derive the pane only from the record target. guard_type returns 1 for that empty target, so the automatic /rename lock never runs for this valid record shape (and SessionStart cannot type it either). Fall back to the current pane id from tmux when the current record has no target.
  gt_pane="${1-}"; gt_text="${2-}"
  [ -n "$gt_pane" ] && [ -n "$gt_text" ] || return 1
  command -v tmux >/dev/null 2>&1 || return 1
  gt_cmd="$(tmux_window_field "$gt_pane" '#{pane_current_command}' 2>/dev/null || :)"
  [ -n "$gt_cmd" ] || return 8
  case "$gt_cmd" in claude|node) : ;; *) return 9 ;; esac

lanes-edit.sh:5537

  • This new safety gate is not exercised by the Amendment 12 tests: every fake window uses the default claude command, so no case proves that a shell/editor (or an unreadable pane_current_command) causes guard_type to type nothing. Since this check is the only barrier against injecting /rename into an unrelated process, add focused cases for the non-accepted and indeterminate command results.
  gt_cmd="$(tmux_window_field "$gt_pane" '#{pane_current_command}' 2>/dev/null || :)"
  [ -n "$gt_cmd" ] || return 8
  case "$gt_cmd" in claude|node) : ;; *) return 9 ;; esac
  tmux send-keys -t "$gt_pane" "$gt_text" Enter 2>/dev/null || return 1
  • Files reviewed: 11/11 changed files
  • Comments generated: 3
  • Review effort level: Lite

Comment thread lane-end
Comment thread lane-start
Comment thread lanes-edit.sh Outdated
…e, `--retire` refuses to name the other half of a live session as its duplicate, and the lock types into the pane tmux names when the record names none

COPILOT ROUND 2 ON #52: three inline threads and five findings suppressed into
the review body, none of them a restatement of round 1. FOUR ARE REAL AND ARE
HERE; four are declined on the PR with the evidence, one line each. Two of the
four are in code this branch wrote LAST round, which is the argument for reading
a second round rather than closing after the first.

1. `not a session record` WAS TOO WIDE A TEST FOR "THE HARNESS'S COMPANION",
   and the counter-example is in this suite already: Amendment 11 clause (k)'s
   section writes an interactive fork with `kind: user`, and
   `record_fields_are_session` answers "not a session" for it. Under last
   round's spelling that record — and every kind a later harness invents —
   arrived at the guard as the benign `bg` companion beside this window's own,
   and two live processes on one transcript went unrefused. Amendment 8 ruling
   (g) names ONE shape to pass over in silence, so that shape is what the test
   matches: `kind = bg`. Everything else live on this id is a duplicate,
   including a record too old to carry a `kind` at all, which pass 1 writes down
   as `interactive` — the closed direction in both cases.

2. `lane-end --retire <pid>` SELECTED ITS TARGET BY PID AND NEVER READ THE
   VERDICT, so the one pid a person can reach for that is neither a duplicate
   nor the lane's own session — the harness's `kind: bg` COMPANION of that very
   session — was reported as a live duplicate and given Amendment 6(d)'s ending.
   That is the other half of the conversation that IS the lane. It is now
   refused by name, in ruling (g)'s own words, with `transcript-holders` named
   as the read that says which process is which and `lane-end <lane>` as the act
   that ends the lane. The guard and `lane-start` already passed it over; this
   is the third surface agreeing with them.

3. THE LOCK HAD A SESSION TO RENAME AND NO PANE TO TYPE INTO. The pane comes
   from the live record's `tmux` field, and the harness has been seen to write
   NONE for a process plainly in a window (Amendment 8 ruling (g)'s third fact)
   — a record `record_is_here` still places HERE, by the pane's own process
   tree. With an empty target `guard_type` returned 1 and the lock printed "tmux
   would not take the keys" for a name it could have fixed. Both callers run
   INSIDE the session's own pane, so `#{pane_id}` with no `-t` is that pane,
   asked of tmux rather than guessed, and M1's condition is still asked of
   whatever it resolves to.

4. M1's GATE HAD NO TEST, and it is the only barrier between this hook and
   somebody else's process: *"only while the pane's current command is
   `claude`"*. Both halves have one now — a pane running `bash` is not typed
   into and says so, and a pane whose command could not be READ is not typed
   into either, which is clause (d)'s direction one surface down.

AND ONE LINE OF DOCUMENTATION THAT WAS A PROMISE THE CODE DOES NOT KEEP:
`lane-end`'s usage offered `--retire <pid|uuid>` for both populations, and a
duplicate cannot be named by a uuid — its uuid IS the row's own, so it could not
pick between the processes carrying it. The usage and the option block now say
which form names which, and the manual carries the same sentence.

COUNTS. `bash tests/test_lane_helpers.sh`: **1815 passed, 0 failed** at
`eedb55f` -> **1833 passed, 0 failed** here (18 assertions for the four
findings above). CI at `eedb55f`: `tests` (the whole
`pytest` suite) **passed in 12m15s**, `tests-no-submodule` passed,
`tests-windows` passed, SonarCloud passed, `tests-macos` still running when this
was written.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m
Copilot AI review requested due to automatic review settings September 14, 2026 22:43
@brettheap

Copy link
Copy Markdown
Contributor Author

Copilot round 2 is answered at 6cf5359: four taken, four declined, the three threads replied to and resolved. It was not a restatement of round 1 — two of its findings are in code round 1 wrote, which is the argument for reading a second round at all.

Taken. The companion test is now kind = bg and nothing else (not a session record was too wide by exactly the kinds nobody has ruled on — this suite's own interactive fork carries kind: user); lane-end --retire refuses the harness's own companion of the live session instead of reporting it as that session's duplicate; the lock types into the pane tmux names when the live record names none (the harness has been seen to write no tmux field for a process plainly in a window, and the lock was printing "tmux would not take the keys" for a name it could have fixed); and M1's gate — "only while the pane's current command is claude" — has cases for both of its halves, a pane running bash and a pane whose command could not be read, which is the only barrier between this hook and somebody else's process.

Declined, with the evidence.

  • lane-end's usage advertises a uuid for a duplicate (suppressed, lane-end:26) — taken as documentation rather than code: a duplicate's uuid IS the row's own and could not pick between the processes carrying it, so the usage line, the option block and the manual now say a FORK is named by pid or uuid and a DUPLICATE by pid.
  • (not asked) falls into the generic window arm (suppressed, lane-end:684) — that arm is the documented intent, and the behaviour asked for is the behaviour there: (not asked) sets dr_own=1, which is what refusing to retire an unknown means (Amendment 18(b): a window that could not be asked about is UNKNOWN and not dead, and only a (gone) this server actually refuted makes a record retirable). The nit is the refusal's wording — it says "IS lane X's own live session" where it means "could not be told apart from it" — and that is a message, reachable only with no tmux at all, where this act is being typed from a bare shell.
  • The literal-prefix fast path in under_projects_root (suppressed, lanes-edit.sh:5278) — for the prefix to match while $PROJECTS_ROOT is missing, a LIVE session's own cwd would have to exist under a parent that does not, which cannot happen; and the resolution beneath it exists for the launcher's symlinked projects root, where erring toward "inside" is the deliberate direction (the other direction is the guard never firing at all).
  • A per-workstation lock spanning the resume (lane-start:1807) — answered on its thread: 18(h) asks for a count, every read here is a snapshot, and a lock that has to be released at the exec it protects protects nothing.

bash tests/test_lane_helpers.sh: 1815 → 1833 passed, 0 failed. CI at eedb55f: tests (the whole pytest suite) passed in 12m15s, tests-no-submodule, tests-windows and SonarCloud passed.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate findings remain, including a duplicate-holder safety gap.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (5)

lanes-edit.sh:6038

  • When the yes path creates a brand-new destination row, lane-start --no-launch can leave its session cell as pending — set by the session's first act: the current UUID belongs to the source row, so the bind veto prevents harness_sid, and a failed UUID mint leaves new_sid empty. This code recognizes only none recorded, so it cannot append the current UUID; the offer has already been removed and the source marked MOVED, leaving every subsequent prompt in the destination lane refused. Treat the exact pending marker as a valid anchor before consuming the offer.
  if [ -z "$gbu_anchor" ]; then
    gbu_cell="$(row_cell "$(row_of_lane "$gbu_to" 2>/dev/null || :)" 3 | sed -e 's/^ *//' -e 's/ *$//')"
    case "$gbu_cell" in
      "none recorded") gbu_anchor="none recorded" ;;
    esac

lanes-edit.sh:5475

  • here is set either by a matching tmux window or by pid_under, so it can be yes even when th_tgt is a stale target for another window. The guard and SessionStart callers then extract the pane from that target and guard_type only checks that the target currently runs claude/node; a reused target can receive /rename instead of the asking session's pane. When ancestry is what proves here, resolve the current pane from the hook context (or validate that the recorded pane belongs to the current window) before typing.
    th_here=no
    if [ -n "$th_tgt" ] && [ -n "$LANES_THIS_WINDOW" ] && [ "${th_tgt%.*}" = "$LANES_THIS_WINDOW" ]; then
      th_here=yes
    elif pid_under "$th_pid" "$LANES_PANE_PID"; then
      th_here=yes

lanes-edit.sh:5433

  • This new all-profile sweep delegates to session_files, whose roots are hard-coded to $HOME/.claude-profiles/profiles and $HOME/.claude/sessions; it does not honor the documented CLAUDE_PROFILES_HOME or an arbitrary CLAUDE_CONFIG_DIR. On a workstation using a custom profiles root, transcript_holders can return no records, causing the guard to refuse the real session and missing cross-profile duplicates. Make the sweep include the configured profile root and current config directory.
  if ! session_files > "$th_files"; then
    rm -f -- "$th_files" "$th_match" "$th_err"
    return 1
  fi

lanes-edit.sh:6106

  • When the destination lane already has a recorded transcript, this invokes the normal lane-start --no-launch resume path. That path still appends a RESUMED by <destination-id> status even though no resume was launched, and guard_bind_uuid then appends the source session id to the cell. The destination log therefore claims a different transcript was resumed while the row ends on the moved session, leaving the move history inconsistent. Use a move/bind path that records the current $G_ID (or suppresses the normal resume stamp) instead of reusing the ordinary resume bookkeeping.
    "$ga_start" --no-launch $(lane_start_args "$ga_to") >&2 || ga_rc=$?
    if [ "$ga_rc" != 0 ]; then
      guard_refuse "\`lane-start --no-launch $(lane_start_args "$ga_to")\` exited $ga_rc (its own words are above), so this window has NOT moved and $ga_from has NOT been marked MOVED. The offer is KEPT, so answer \`yes\` again once that refusal is settled, or \`no\` to stay $ga_from."
      return 2
    fi
    rm -f -- "$ga_f"
    guard_triple
    note "MOVED: this window is now lane $ga_to — renamed, and stamped. The prompt that answered the question is consumed; send your work again."

openRepoTools:760

  • The pair merge is new, but the planning failures above it (missing jq, invalid JSON, or a non-object hooks) still say they will merge one SessionStart entry and print only hook_block_text. When installation fails, the new UserPromptSubmit guard is therefore omitted from the diagnosis even though the all-or-nothing plan now covers both entries. Update all of those refusal messages to describe both entries and include the guard block.
	# BOTH MERGES ARE COMPUTED BEFORE EITHER IS PLACED, into one staged file, so
	# the all-or-nothing rule covers the PAIR: a settings file that took the
	# SessionStart entry and refused the guard is a half-install of a hook pair,
	# and nothing on screen would say which half.
  • Files reviewed: 11/11 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread lanes-edit.sh
brettheap and others added 4 commits September 14, 2026 22:57
…es instead of a uuid, the lock never types into a pane a stale target names, and `--install`'s three planning refusals print both entries because the plan is a pair

COPILOT ROUND 3 ON #52: one inline thread and five findings suppressed into the
review body. THREE ARE REAL AND ARE HERE; three are declined on the PR with the
evidence. This round is the last: its inline finding is round 2's own, narrowed
one step further and declined for a reason that is about blast radius rather
than about the shape of a record.

1. `pending — set by the session's first act` IS THE OTHER TEXT A SESSION CELL
   CAN CARRY, and it is exactly the one a `yes` to a fresh lane can leave.
   `lane-start` writes it into a row it creates with neither a minted uuid nor
   one it was allowed to take (`lane-start:1914`) — and after a rename, veto 1
   refuses this window's uuid because the row being LEFT still records it. The
   guard's own last write (Amendment 12(h) rule 2's *"row X stamped with this
   uuid"*) anchored only on `none recorded`, so on that row it could not be made
   at all: the offer was consumed, the source was marked MOVED, and the next
   prompt refused for ever on a state the guard had created by obeying the
   person. That is the loop this branch fixed one commit ago, reached through the
   one cell text the fix did not list. Both texts are anchors now, and the case
   drives it end to end: a row whose cell carries no uuid, a `yes`, and the cell
   ending on this transcript afterwards.

2. THE LOCK TYPED INTO WHATEVER PANE THE RECORD NAMED, and `here` does not mean
   the record's target is this window: it is true by the target OR by the pane's
   own process tree, and tmux REUSES window ids. So a record that is here BY
   ANCESTRY could carry `othersess:@5.%5` — a pane some other session is sitting
   in — and `/rename <lane>` would be typed into it. M1's gate cannot catch that
   one, because that pane is running `claude` too. The pane is now taken from the
   record ONLY where its target names this window; otherwise it is left empty and
   `guard_type` asks tmux for the pane this hook is running in, which is the
   session's own. `ssb_name_line` takes the same fence, with `here_context` asked
   beside it because the read above it runs in a subshell.

3. `--install`'s THREE PLANNING REFUSALS DESCRIBED A PLAN THAT NO LONGER EXISTS:
   no `jq`, a `settings.json` that is not valid JSON, and a `hooks` that is not
   an object each said they would merge ONE `SessionStart` entry and printed one
   block. The plan has been a PAIR since Amendment 12 adoption act 3, and a
   person who cannot install reads exactly those three messages. All three name
   both entries and print both blocks.

DECLINED, AND THE INLINE ONE IS THE INTERESTING DECLINE: that the companion test
should also require no `tmux` target and no `nameSource`, the exact shape ruling
(g) describes. It adds no detection — the duplicate this rule exists to catch,
measured four times on 2026-09-14, is a `bg` record with neither field — and it
adds a way for a HARNESS RELEASE to block every prompt on a workstation: this
file's own `window_session` anticipates "the moment a harness starts writing a
target on to companions", and on that day every lane session with a companion
beside it would become "a second live process" and refuse every prompt until
somebody edited the tooling. `kind` is the field ruling (g) makes the
discriminator and `record_is_session` has implemented since Amendment 8; the
other two are description.

COUNTS. `bash tests/test_lane_helpers.sh` was **1833 passed, 0 failed** at
`6cf5359`, and the figure for THIS round is measured on the merged tree in the
commit that follows it: `main` moved to `b5244e2` while this round was being
written (#44, the installer's mode stamping and unwritable-target refusal),
which touches `openRepoTools` — a file this round also touches — so the suite
run that means anything is the one after the merge. CI at `6cf5359`: `tests` (the whole
`pytest` suite) passed in 10m42s, `tests-no-submodule` in 10m21s,
`tests-windows` and SonarCloud passed, `tests-macos` still running when this was
written; every check passed at `eedb55f` before it, macOS included.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m
…d) into the Amendment 12 branch — three conflicts, all of them the two rounds writing about the same `--install`, and the macOS job's three red lines taken with them

MERGE AND NEVER A REBASE: this branch is pushed and reviewed.

THE THREE CONFLICTS, and each resolution keeps BOTH sides rather than picking:

1. `openRepoTools`'s `jq`-missing refusal. This branch made it name the PAIR of
   entries and print both blocks; `main` added a REGULAR-FILE check for
   `~/.claude/settings.json` immediately under it (#44 round 1: `chmod` follows
   a symlink and `mv -f` swallows one). Both are kept, and that check's own
   refusal is respelled for the pair — it merges two entries, not one.

2. `openRepoTools`'s `present` arm. This branch split it in two, one line per
   entry, and made the WRITE conditional on either being absent, so a person's
   settings never pass through a state carrying one of the pair; `main` gave the
   arm a `chmod 600` for the mode a `present` run never applied (#40 finding 3).
   Both: the `SessionStart` arm keeps `main`'s mode restoration verbatim — its
   test asserts the sentence — the guard arm says what it found and does NOT
   chmod a second time (the mode belongs to the FILE, not to either entry), and
   the one write follows them.

3. `README.md`'s install paragraph and the cap that counts it. Both rounds
   rewrote the same sentences: two merged entries with the name guard's own
   description (here) and the regular-file rule with the modes `--install`
   stamps (`main`). The merged paragraph carries both, which is longer than
   either half, so the cap is the count of WHAT MERGED — 417 and 428 -> 436 —
   with both dated entries kept and a third naming the merge, which is the rule
   the 2026-09-13 entry states in its own words.

AND THREE COUNTS `main` STATES THAT THE SECOND HOOK ENTRY MOVES: "eleven of the
eighteen artifacts", "seventeen of the eighteen artifacts", and the comment
calling `settings.json` "the eighteenth artifact" — it carries the eighteenth
AND the nineteenth since adoption act 3. `HOOK_ENTRIES = 2` already derives the
total in `tests/test_openrepotools_command.py`; these are the prose beside it.

THE macOS JOB'S THREE RED LINES AT `6cf5359`, WHICH ARE THIS BRANCH'S AND NOT
THE MERGE'S. `tests-macos` failed where every other job passed, on the one case
round 2 added for the lock's pane fallback: the fixture made the live record's
pid a CHILD of the fake pane's, and `pid_under` climbs `/proc` to answer that —
which macOS does not have. The record was "here" on Linux and nowhere on the
macOS runner, so the guard refused with "no live session record in THIS window
carries this session id" and the lock never ran. The fixture now gives the pane
the record's OWN pid, which `pid_under` answers at its first step with no walk
at all, and the case says why in its own comment. THE BEHAVIOUR WAS NEVER
PLATFORM-SPECIFIC: `guard_type`'s fallback asks tmux for `#{pane_id}`, and the
same is true on both.

The submodule is checked out at the pin `main` brought (`39d5c98`).

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m
…skill) into the Amendment 12 branch — no conflict, and the suite's two new sections sit beside each other

MERGE AND NEVER A REBASE, for the same reason as the merge under this one: the
branch is pushed and reviewed.

NOTHING CONFLICTED. #53 adds 59 lines to `tests/test_lane_helpers.sh` in the
`/restart` skill's own section and touches `skills/restart/SKILL.md`,
`tests/test_openrepotools_command.py` and `tests/test_repo_hygiene.py` — none of
them a file or a section this branch writes in. The merged suite runs both
rounds' cases, and the run behind this commit is the one that proves it.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m
…s recovery) into the Amendment 12 branch — no conflict, and the third main this branch has met tonight

MERGE AND NEVER A REBASE, for the third time and the same reason: this branch
is pushed and reviewed, and `main` moved under it three times while one review
round was being answered (#44's installer round, #53's `lread` fence, and now
#50).

NOTHING CONFLICTED. #50 rewrites `commit_push`'s true-conflict `die` in
`lanes-edit.sh` and adds a `#32` section to `tests/test_lane_helpers.sh`, and
this branch's own writing in both files is elsewhere: the guard and its reads
sit below every function #50 touches, and the Amendment 12 section sits between
the Amendment 11 and Amendment 15 sections rather than at the end where #50's
went. `docs/README-lanes.md` took both rounds' paragraphs without overlap.

The suite run behind this commit is on the merged tree.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m
@brettheap

Copy link
Copy Markdown
Contributor Author

Copilot round 3 is answered at e2b6694, which also carries three merges of main — b5244e2 (#44's installer round and #46's pin), 66b4fb9 (#53) and a954435 (#50) — and the fix for the macOS job's three red lines at 6cf5359, which were this branch's own.

The macOS failure, and it was the fixture and not the behaviour. Round 2's case for the lock's pane fallback made the live record's pid a CHILD of the fake pane's, and pid_under climbs /proc to answer that — which macOS does not have. The record was "here" on Linux and nowhere on the macOS runner, so the guard refused with "no live session record in THIS window carries this session id" and the lock never ran. The fixture now gives the pane the record's OWN pid, which pid_under answers at its first step with no walk at all, and the case says so in its own comment. Every other job was green at that commit.

Taken from round 3 (three, all suppressed into the review body):

  1. pending — set by the session's first act is the other text a session cell can carry (lanes-edit.sh:6038), and it is exactly the one a yes to a fresh lane can leave — veto 1 refuses this window's uuid because the row being LEFT still records it. The guard's own last write anchored only on none recorded, so on such a row it could not be made at all and the next prompt refused for ever on a state the guard created by obeying the person. That is the loop this branch fixed one commit earlier, reached through the one cell text the fix did not list. Both texts are anchors now, and a case drives it end to end.
  2. The lock typed into whatever pane the record named (lanes-edit.sh:5475). here is true by the record's target OR by the pane's own process tree, and tmux reuses window ids — so a record here BY ANCESTRY could carry othersess:@5.%5 and /rename would land in somebody else's pane, which M1's gate cannot catch because that pane is running claude too. The pane is now taken from the record only where its target names THIS window; otherwise guard_type asks tmux for the pane this hook runs in. ssb_name_line takes the same fence.
  3. --install's three planning refusals described a plan that no longer exists (openRepoTools:760) — no jq, invalid JSON, a non-object hooks — each said it would merge ONE SessionStart entry and printed one block. All three name both entries and print both blocks.

Declined (the inline one and two suppressed):

  • The companion should also require no tmux and no nameSource (lanes-edit.sh:5495) — answered on the thread: it adds no detection (the duplicate this rule exists to catch is a bg record with neither field, measured four times on 2026-09-14) and it adds a way for a HARNESS RELEASE to block every prompt on a workstation, which this file's own window_session comment anticipates by name. kind is the field ruling (g) makes the discriminator.
  • session_files's roots are hard-coded (lanes-edit.sh:5433) — that read predates this PR and is shared by live_holder, forks, window-session and who; changing which directories the estate's liveness reads sweep is a change to all of them and not to this one.
  • The destination log says RESUMED by <another id> after a move (lanes-edit.sh:6106) — real, and filed as The offer's yes leaves the destination lane's log saying a transcript was RESUMED that nobody is in, because lane-start has no binding mode and the guard makes the cell write itself #70 rather than taken here: the fix is a BINDING mode for lane-start (the act clause (h) rule 2 actually asks for), not a change to the guard. The cell — which every later read follows — does end on the moved transcript, and the guard prints what it wrote.

bash tests/test_lane_helpers.sh on the merged tree: 1870 passed, 0 failed.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate findings remain in the guard and session-start logic.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (3)

lanes-edit.sh:5510

  • record_window_state uses status 1 for both a positively rejected window and a tmux read that returned no answer, but this branch labels every status-1 result as (gone). With a silent/unavailable tmux server, lane-end --retire can consequently treat the lane's own session as non-owning and print a retire/rename act instead of refusing an indeterminate read. Preserve a distinct indeterminate result and only use (gone) after the window was actually refuted.
      case "$th_wrc" in
        0) th_where="window ${th_tgt%.*}" ;;
        1) th_where="window ${th_tgt%.*} (gone)" ;;
        *) th_where="window ${th_tgt%.*} (not asked)" ;;
      esac

lanes-edit.sh:6275

  • This SessionStart path runs before the next-prompt guard and unconditionally types /rename $snl_lane for every name mismatch, including a newer nameSource:user rename. If a person renames a session to another lane and then resumes or clears it, this silently erases the choice before guard_run can create the promised move offer. Leave newer user renames for the prompt guard to offer, and auto-lock only derived/older drift.
  guard_type "$snl_pane" "/rename $snl_lane" || snl_trc=$?
  if [ "$snl_trc" = 0 ]; then
    printf "session name '%s' was not the lane '%s' — renamed\n" "$snl_name" "$snl_lane"
  else
    printf "session name '%s' is not the lane '%s' — run: /rename %s\n" "$snl_name" "$snl_lane" "$snl_lane"

tests/test_install_skill_and_hook.py:56

  • The module docstring at lines 2–19 still describes a single merged SessionStart entry and a single hook, while this new block adds and tests a second UserPromptSubmit entry. Please update that module-level description so maintainers are not told that the installer/test contract contains only one hook entry.
#: AMENDMENT 12's SECOND ENTRY, adoption act 3: the NAME GUARD under
#: `UserPromptSubmit`, `~/projects/xFactory/lanes-edit.sh guard`, timeout 5.
  • Files reviewed: 11/11 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread lanes-edit.sh
Comment thread lanes-edit.sh Outdated
…asured on Eagle tonight say `node` bought nothing — and the `SessionStart` hook stops typing over a rename a PERSON made, which is the guard's to OFFER

COPILOT ROUND 4 ON #52: two inline threads and three findings suppressed into
the review body. THREE ARE REAL AND ARE HERE; two are declined on the PR with
the evidence, one of them because it describes a state that cannot arise.

1. `node` IS NOT `claude`, AND M1 NAMED ONE COMMAND: *"only while the pane's
   current command is `claude`"*. This gate read `claude|node` on the reasoning
   that the harness is a node program and `#{pane_current_command}` reports the
   process rather than the wrapper. MEASURED tonight on Eagle, across the twelve
   panes tmux had: eleven lane panes report `claude`, two report `bash` (a
   session under a launcher wrapper) and one `zsh` — and NOT ONE reports `node`.
   So the second word bought nothing here and would have let a stray Node REPL
   sitting in a pane take `/rename <lane>` and an Enter as input. A pane this
   gate refuses is not a lock that failed: the caller prints the line for the
   person, which is the cure — and on this estate today the `bash` panes are
   exactly the windows where they will read it.

2. THE `SessionStart` HOOK WAS UNDOING THE PERSON'S OWN INSTRUCTION BEFORE
   ANYBODY WAS ASKED. Clause (f) types the rename for (h)1's case — DRIFT, a
   name that is no lane's — while (h) rule 2 makes a record whose `nameSource`
   is `user`, whose name is another lane's, and whose `nameSince` is later than
   this window's binding an INSTRUCTION, answered `yes` or `no` at the next
   prompt. This hook runs on every startup, resume, clear and fork, so a rename
   followed by a `/clear` had the name typed back before `guard_run` could put
   the question, and the offer never happened. It is SAID there now and left to
   the prompt that follows, in the same words the guard will use.

3. `tests/test_install_skill_and_hook.py`'s MODULE DOCSTRING still told its next
   reader that `--install` merges ONE `SessionStart` entry, three commits after
   this branch made it a pair and seven tests into the pair's own section.

DECLINED. That the `bg` companion should be classified BEFORE the `here` branch:
a `bg` record whose target is this window or whose pid is under this pane is
this session's OWN companion — Amendment 8 ruling (g)'s *"one session, not a
queue of rival holders"* — and `here` and `companion` are treated identically by
every caller (both are passed over in silence), so the order changes no
outcome; what the reordering WOULD do is make this window's own companion a
`duplicate` on a workstation where `record_is_here`'s tier 3 is the only thing
that answers. And that `record_window_state` should tell a silent tmux from a
refuted window: `(gone)` and `(not asked)` already differ, `lane-end` protects
the second, and separating a dead SERVER from a dead WINDOW needs a second
question put to tmux — which is a read this file does not make anywhere yet and
a change to `who`'s liveness reading as much as to this one, so it is named on
the PR rather than written in a review round.

COUNTS. `bash tests/test_lane_helpers.sh`: **1870 passed, 0 failed** at
`e2b6694` -> **1875 passed, 0 failed** here. CI at `e2b6694`: every check green,
`tests-macos` included (22m13s) — which is the run that proves the fixture fix
this branch made for the three red lines it had there.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m
Copilot AI review requested due to automatic review settings September 14, 2026 23:56
@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

Copilot round 4 is answered at 9059f38 — three taken, two declined, both threads replied to and resolved. This is the last round this lane takes: the reviews are not converging (12 findings in round 1, 8 in round 2, 6 in round 3, 5 here), and what is left is either declined on the evidence or filed.

Taken

  1. node is not claude, and M1 named one command — "only while the pane's current command is claude". Measured tonight on Eagle across the twelve panes tmux had: eleven lane panes report claude, two report bash (a session under a launcher wrapper), one zsh, and none reports node. The second word bought nothing on the estate it was written for, and would have let a Node REPL in a pane take /rename <lane> as input. A pane the gate refuses still gets the line printed for the person — on this estate the bash panes are exactly where it will be read.
  2. The SessionStart hook was undoing the person's own instruction before anybody was asked. Clause (f) types the rename for (h)1's case — DRIFT, a name that is no lane's — while (h) rule 2 makes a nameSource: user rename to another lane's name an INSTRUCTION answered at the next prompt. This hook runs on every startup, resume, clear and fork, so a rename followed by a /clear had the name typed back before guard_run could put the question. It is said there now and left to the prompt that follows.
  3. tests/test_install_skill_and_hook.py's module docstring still told its next reader that --install merges one SessionStart entry, seven tests into the pair's own section.

Declined

  • Classify the bg companion before the here branch (lanes-edit.sh:5535) — answered on the thread: here and companion are the same answer to every caller (both are passed over in silence), so the order changes no outcome; what it WOULD change is turning this window's own companion into a duplicate wherever the same-profile test does not hold, which is the false refusal round 2's narrowing exists to avoid. The duplicate this rule catches is never here.
  • record_window_state should tell a silent tmux from a refuted window (suppressed, lanes-edit.sh:5510) — (gone) and (not asked) already differ and lane-end protects the second; separating a dead SERVER from a dead WINDOW needs a second question put to tmux, which is a read this file makes nowhere yet and a change to who's liveness reading as much as to this one. Worth doing, not worth doing inside a review round.

bash tests/test_lane_helpers.sh on the merged tree: 1875 passed, 0 failed. CI at e2b6694 (the commit under this one): every check green, tests-macos included (22m13s), which is the run that proves the fixture fix for the three red lines this branch had there.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Six unresolved findings remain, including four critical fail-closed and retirement issues and two moderate command and rename issues.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (2)

lanes-edit.sh:6166

  • If tmux rename-window fails, this only logs a note and still invokes lane-start. When the old window name is unregistered (for example, claude), lane-start's window veto does not reject it, so the destination row can be stamped and the source marked MOVED while the actual window keeps its old name; the next prompt remains mismatched. Abort the yes path and keep the offer pending until the rename succeeds.
    if command -v tmux >/dev/null 2>&1; then
      tmux rename-window "$ga_to" 2>/dev/null ||
        note "the window could not be renamed to $ga_to, so lane-start may refuse to take this session as that lane's (its step 3b veto 2)."
    fi

lanes-edit.sh:5917

  • When the session is named for an existing legacy lane such as legacy-ui, lane_start_args returns --dir <path> legacy-ui. This branch emits that placeholder after run: as the only cure, so the blocking refusal gives the person a command that cannot be run. Reuse the guard_args_filled/legacy-lane handling used by the offer and explain that the checkout path must be supplied instead of presenting the placeholder as a completed command.
      guard_refuse "this window is named '$G_WINNAME', which is no lane, while the SESSION is named for lane $G_SES_LANE — so nothing binds this conversation to a row, no surface would refuse a second window taking the same lane, and the handoff this session writes is one only this profile can find. THAT IS THE 2026-09-10 CASE, which ran for three days." "lane-start --no-launch $(lane_start_args "$G_SES_LANE")"
  • Files reviewed: 11/11 changed files
  • Comments generated: 4
  • Review effort level: Lite

Comment thread lane-end
Comment thread lane-end
Comment thread lanes-edit.sh
Comment thread lanes-edit.sh
@brettheap
brettheap merged commit 2faa883 into main Sep 15, 2026
7 checks passed
@brettheap
brettheap deleted the feat/amendment-12-name-guard-lock branch September 15, 2026 00:25
brettheap added a commit that referenced this pull request Sep 15, 2026
…ipt-holders`, `lane-end --retire` and the installer's nineteenth artifact): THREE conflicts, all three UNIONS, because the two amendments add to the same three lists and contradict none of them

`origin/main` moved a third time under this branch, and this time it moved in
every file this one rewrites. `git merge` resolved all but three hunks; each of
those three is a LIST that both sides extend.

1. **`lanes-edit.sh`, the workspace exemption.** Amendment 12(d) exempts `guard`
   because only exit 2 blocks a `UserPromptSubmit` and a 1 would print the
   workspace refusal and let the prompt through; Amendment 18 Addendum 1 exempts
   `lane-groups` and `next-free` because they read their rows from STDIN and
   touch no workspace at all. Neither reason is the other's, and neither is in
   the other's way: `session-start|guard|lane-groups|next-free`.
2. **`lanes-edit.sh`, the unknown-subcommand refusal.** The same union, with
   #52's `guard` and `transcript-holders` and this branch's `lane-groups` and
   `next-free` — the one line in this file that has to name every subcommand
   there is, which is why both sides touched it.
3. **`openRepoTools`, the artifact paragraph.** #52 takes it from EIGHTEEN to
   NINETEEN (the `UserPromptSubmit` entry beside the `SessionStart` one, so TWO
   merged entries) and this branch's edit was to stop the same paragraph naming
   `restart` as the second word on PATH. Both: nineteen, two entries, and the
   second word named as what it is now — *"`restart` then, `lane` since
   Amendment 18 Addendum 2 retired that one"*. The count is still derived from
   the lists and never restated from memory.

WHAT THE MERGE DID NOT HAVE TO RESOLVE, and is worth saying because it is where
a conflict would have mattered: `INSTALLABLES` is unchanged at ELEVEN with
`lane` in `restart`'s place, `SHIPPED_BASH` is the same six, and the CI parse
list names `lane` and no `restart`. #52's `lane-start` additions (`dup_check`
calling the guard at both sites) and this branch's bare-alias rung are different
arms of that file and merged cleanly, as did #52's 712 new lines of suite beside
this branch's Addendum 1 section.

Measured on the merged tree, in this worktree:

    bash tests/test_lane_helpers.sh   2095 passed, 0 failed, 0 skipped,
                                      0 pending  (+169 over this branch before
                                      the merge, which is #52's own section)
    python3 -m pytest tests/test_repo_hygiene.py \
      tests/test_openrepotools_command.py \
      tests/test_install_skill_and_hook.py \
      tests/test_wip_init_command.py \
      tests/test_upstream_pin.py -q                          288 passed

The full `python3 -m pytest tests -q` under this workstation's lock follows on
this tree; CI's own `tests` job is the same command.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m
brettheap added a commit that referenced this pull request Sep 15, 2026
…HREE independent duplicate reads in sequence, none replacing another

#52 landed on main while this PR was on its own round 7 and touches the exact
same `if [ -n "$retire_target" ]; then` block in lane-end: #52 adds its OWN
Amendment 18(h) duplicate check (`last-session` + `transcript-holders`,
finding any live SESSION RECORD that carries the row's own id, reported and
never killed -- refusing the lane's own window session and the harness's
`kind: bg` companion by name) at the same point this PR added its own
(`duplicate-holder`, finding a live `bg-pty-host` wrapper in the PROCESS
TABLE by its `--fork-session --resume <uuid>.jsonl` argv, and TERMing that
pair on sight because nothing is answering a prompt in it). Neither branch
knew about the other's read.

Resolution: keep both, run in sequence, neither replacing the other --
forks (title, never killed) -> duplicate-holder (process table, TERMed) ->
transcript-holders (session record, never killed) -- and only the
end-of-block report/die names all three surfaces when none of them matches
`$retire_target`. `transcript-holders`' own local exit-status variable is
renamed dh_rc -> thh_rc where it is read, so it cannot be mistaken for
duplicate-holder's `dh_rc` a few lines above; `last-session` also gets the
`LANES_NO_FETCH=1` its sibling reads in this same function already use, since
`forks` already fetched for this checkout. The lane-end usage text and the
lanes-edit.sh subcommand list are merged the same way -- three named reads,
not two competing ones.

docs/README-lanes.md, lane-start, openRepoTools, tests/test_install_skill_and_hook.py,
tests/test_openrepotools_command.py and tests/test_repo_hygiene.py merged
without conflict. tests/test_lane_helpers.sh's cleanup() keeps both PR's
fixture pids (DUP_PARENT/DUP_CHILD and GD_DUP).

No suite run yet behind this merge -- that follows in the next commit(s).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m
brettheap added a commit that referenced this pull request Sep 15, 2026
…eeded one, and Copilot round 8 caught two more gaps in the dead-lane path itself -- all three, plus what they led to, fixed here

THE CI FAILURE (tests-no-submodule, e4f9054): `duplicate_holder_pids`'s
`dhp_out` (lanes-edit.sh) joined its three fields with a literal TAB, and a
bare TAB is one of bash `read`'s own IFS-WHITESPACE characters -- so
`IFS=$'\t' read -r a b c` on `"<parent>\t\t<uuid>"` (the DOCUMENTED shape for
a parent with no confirmable child, not a rare race) collapsed the two
adjacent tabs into ONE delimiter, exactly as it collapses repeated spaces,
shifting the uuid into `b` and leaving `c` empty. Measured: a still-alive
child was reported `pid <uuid> (child): gone`, and its genuinely-TERMed
parent `STILL ALIVE, TERM withheld` (its own argv check compared against the
emptied `$dh_uuid`). Fixed by switching the wire format to `$US`/`$'\037'`,
which is not IFS whitespace and never collapses -- the same reason
`live-holder`'s own multi-field line already uses it. Two consumers in
lane-end updated to match. The round-7 RACE fixture that caught this also had
its own bug: `FAKE_PGREP_F_RACE` omitted `$RACE_CHILD`, so discovery's own
"is this child ALSO a fork-session candidate" fence never passed and the
child slot was empty for the wrong reason; added, and the `hasnt` assertion
checking a literal tab in `duplicate-holder`'s raw output now checks `$US`.

COPILOT ROUND 8, the two posted threads:
* lanes-edit.sh ~2373 (`holder_is_dead`): a line whose payload opens `fork `
  was never excluded from the last-lane-kind-verb scan, unlike
  `lane_row_facts`'s identical exclusion a few hundred lines above it -- a
  legacy `RETIRED ... lane:<lane> -> fork <sid>; ...` line (nothing writes
  one any more, but these logs are append-only) would read a PAUSED, merely
  swapped lane as dead and let `claim --force` take over holds Amendment 8
  exists to protect. Fixed with the same `substr($8,1,5)!="fork "` guard.
  Declined the "also appears at line 4647" half of that comment, with the
  evidence: that line is inside `duplicate_holder_pids`'s unrelated
  "arbitrary child never guessed" block, no verb-scan there at all.
* lanes-edit.sh ~2161 (`lane_states_on`'s own "R18" docstring): still said a
  takeover's tracked verb was "the CLAIMED a takeover displaces", singular,
  after the awk fragment it describes had already grown the full open-verb
  set. Rewritten to name all five and cross-reference the dead-lane
  exception; the `superseded_by` header a few lines below it (also named by
  this thread) already had round 7's correct explanation.

SUPPRESSED COMMENTS, taken:
* docs/README-lanes.md's "Known, deliberate" section still said `--force`
  reaches only a stale `CLAIMED`, not the dead-lane exception just below it.
* lanes-edit.sh's dead-lane takeover was regression-tested for OPENED and
  CLAIMED only; added LANDING and WITHDRAWN cases (repoL-1..4).
* `duplicate_holder_pids`'s child was accepted on fence membership alone; a
  wrapper with two fork-session-flavoured children could attach the wrong
  one. A sole fenced candidate is still accepted without a `ps` call (so the
  race case above keeps working with no `ps` read on the child at all); two
  or more now each prove their OWN `--resume` target before one is named.
  Tested with a wrapper with two children, the wrong one listed first by
  `pgrep -P`.
* The `--force` verb-mismatch die named "OPENED, LANDING or WITHDRAWN" but
  omitted TAKEOVER, which a dead lane's own un-superseded takeover can also
  be by the time this branch is reached.
* `RACE_PARENT`/`RACE_CHILD` were not in `cleanup()`'s trap; added beside
  `DUP_PARENT`/`DUP_CHILD`.

WHAT REGRESSION-TESTING THE THIRD MISSING VERB FOUND, past what review named:
a dead lane's own un-superseded TAKEOVER (repoL-5, over repoL-0) IS
takeable -- the write is correct, `who` before was the miscount -- but
`who opensoft/repoL#40` afterward reported ZERO holders, not one:
`superseded_by`'s "does some OTHER lane's last line here read TAKEOVER" test
does not stop at the current one, so repoL-5's now-stale TAKEOVER was
misread as proof that repoL-6 (which just took over FROM repoL-5) is ALSO
superseded. Pre-existing in `superseded_by`'s own design -- the open-verb
broadening (issue #30) is what makes a CHAIN of two takeovers reachable from
one ordinary `--force`, where before only the simultaneous-race shape
(`repoG-1`/`repoG-2`, already documented) could produce two TAKEOVER-ending
lanes on one object. Filed as #74 and claimed under
this lane rather than guessed at here; the test now asserts the actual
(wrong, but honest) zero-holder count and cites the issue, and
docs/README-lanes.md gets a "Known, NOT deliberate" paragraph beside the
existing race one.

WHAT MERGING #52 INTO THIS PR'S OWN RETIRE PATH FOUND, once the two branches'
duplicate reads actually ran in sequence rather than in two different repos:
* `lane-end`'s `transcript-holders`-based dup check (#52) reads EVERY live
  session record on the transcript, including a lane's own perfectly
  ordinary one -- and since `lane-end` is a plain script, never itself a
  bound claude session, that record verdicts `unrelated` rather than
  anything recognisable as "mine". Treating raw `dup_rows` as "something is
  live here" made a healthy lane's `--retire <bogus pid>` answer 2 ("neither
  ... nor a duplicate") instead of the honest 8 ("nothing of any kind is
  live"), naming a "duplicate" that was only ever the lane's own session.
  Fixed with a `dup_other` fence keyed on `$lh_pid` -- already proven a few
  lines above by `live-holder`, the one liveness implementation this file
  has -- so a row that IS the lane's own known session is accounted for and
  never mistaken for "something else is here".
* repoGD-1's own-session refusal (an existing #52 test) expected
  `transcript-holders`' wording, but this PR's own general-purpose
  `live-holder` check (`$lh_pid == $retire_target`) now runs first in the
  merged sequence and answers with ITS wording instead -- both refuse, both
  say why, only one gets to speak first. Test updated to the wording that
  actually fires.

Suite: `bash tests/test_lane_helpers.sh`: 1940 passed, 0 failed, 0 skipped,
0 pending (from 1703 before this round's additions).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m
brettheap added a commit that referenced this pull request Sep 15, 2026
…gers WRAPPED into a lane nobody picked, `f` told an un-upgraded workstation to read something that does not exist there, and `lane ""` answered a different question

**`lane:1215` — A DIGIT STRING TOO LONG TO BE A POSITION IS NOT A POSITION.**
`pick=$((10#$answer))` WRAPS at the shell's integer width: `18446744073709551617`
comes back as `1`, so an answer far outside `1-N` reached the range check as a
number that IS in it and would have bound a lane nobody picked. That is the
wrong-lane outcome every fence in this file exists to stop, arriving through the
one door that reads a person's typing. The LENGTH is compared before the
conversion, against the digits of `$n` itself, because that is the widest answer
the question offered.

**`lane:873` — `2` IS THE CONTRACT'S OWN CODE IN THE `f` PATH TOO.** The listing
has told the two statuses apart since round 3 — an un-upgraded workstation named
as one and sent to `openRepoTools --install`, a read that FAILED named by its
code — and this path, the same read one answer later, collapsed both into "run
it by hand". A person who typed `f` on a workstation that has not taken the
install was told to read a subcommand that does not exist there.

**`lane:234` — AN ARGUMENT THAT WAS GIVEN AND IS EMPTY IS NOT "NO ARGUMENT".**
`lane ""` left `$name` empty, every `[ -n "$name" ]` below it read that as "no
lane was named", and a malformed named invocation silently became the LISTING —
and on a terminal the numbered pick, a question the person did not ask. It is
the contract's 64 at the door now, in the same terms the helper's own
whole-string fence uses.

AND THE FOURTH IS THIS PULL REQUEST'S OWN BODY: it said `--install` makes
EIGHTEEN artifacts while the code's invariant became NINETEEN when
`origin/main`'s #52 merged Amendment 12's `UserPromptSubmit` entry in beside the
`SessionStart` one. Corrected there, and derived from `INSTALLED` and
`HOOK_ENTRIES` rather than restated.

New cases: `f` answered on a workstation whose helper has no `next-free` —
refusing with 2, naming the install and starting nothing; the pick answered
`18446744073709551617`, refused as outside `1-N` with nothing bound (it is the
wrap that would have bound something); and `lane ""` and `lane --all ""`, each
the contract's 64, neither of them the listing.

Measured on this tree (`origin/main` merged in at 2faa883):

    bash tests/test_lane_helpers.sh   2120 passed, 0 failed, 0 skipped, 0 pending

CI is green on the head this sits on — `42d243a`, all four jobs, `tests-macos`
included — as it was on `03171c3` and on `619c484` before it.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m
brettheap added a commit that referenced this pull request Sep 15, 2026
…le goes through the launcher in its own directory and profile, LIVE HERE attaches and never starts a second process, bound elsewhere names where; and `restart` leaves the person's PATH in the same breath (Amendment 18 Addendum 1 and Addendum 2) (#45)

* WIP (#43, predecessor's state at the resume): `lane` written whole, `restart` deleted, and every surface re-pointed — committed unrun, so that what one agent left and what the next one changes are two diffs and not one

NOT A CLAIM THAT ANY OF IT PASSES. The predecessor of this session was killed
mid-work with fourteen files modified and none committed, and this commit is
those fourteen files exactly as they were found — `git status` and
`git diff --cached --stat` of the resume, to the byte, with nothing added,
nothing rerun and nothing fixed. It is committed BEFORE the suite is run
precisely so that the next commit on this branch is only what this session
changed: a WIP squashed together with its repairs is a diff nobody can read
against the ruling it answers to.

WHAT WAS FOUND, file by file:

* `lane` — 895 lines, new, 755. The word of Amendment 18 Addendum 1: the
  numbered pick, the three branches (available → the launcher in the lane's own
  recorded directory and profile; LIVE HERE → `move-window -a` + `select-window`,
  or `select-window` alone where the window is already in this session, or
  `tmux attach -t <session>` outside tmux, or `switch-client` under `--switch`;
  bound elsewhere → the refusal naming where), `lane <name> [<profile>]`,
  `--all`, `--dir`, `--dry-run`, and the no-terminal form that lists, suggests
  and asks nothing.
* `restart` — DELETED, 545 lines. Amendment 18 Addendum 2, in force since
  2026-09-14T16:50:32Z.
* `lanes-edit.sh` — `lane_groups` and `lane_next_free` added after `lanes_rows`,
  both reading their rows from STDIN; their two subcommands `lane-groups` and
  `next-free` added, exempted from the workspace fence beside `session-start`,
  and named in the unknown-subcommand refusal; column 10 now spells
  `lane <lane>`.
* `lanes` — its own next-free loop removed in favour of the helper's read, the
  `restart` hints re-pointed, `bind it:` in place of `restart:`, and a footer
  that says so where the helper predates the read.
* `lane-start` — the bare form `exec`s `lane` (clause (i-4)).
* `openRepoTools` — `INSTALLABLES` swaps `restart` for `lane`; ELEVEN STILL,
  because one word left as another arrived.
* `.github/workflows/tests.yml` — the macOS bash-3.2 parse list follows.
* `README.md`, `docs/README-lanes.md`, `skills/lane-swap/SKILL.md`,
  `skills/restart/SKILL.md` — the prose, including the manual's new
  "One word: `lane`" section and "Bind and list (Amendment 11, amended by
  Amendment 18)".
* `tests/test_lane_helpers.sh` — +551/-177: decision 7's section re-pointed at
  `lane <name>`, and a new section "Amendment 18 Addendum 1: the word `lane`,
  the numbered pick, the three branches" with six seeded lanes covering the
  five states, a fake tmux of its own, and a pty harness that answers the one
  question for real.
* `tests/test_openrepotools_command.py`, `tests/test_repo_hygiene.py` — the
  installed list, `SHIPPED_BASH`, `FORK_SURFACES`, the usage/parser
  parametrization and the lane-name fence test, all moved to `lane`.

The base is `main` at 92eab6c. Amendment 15 (`canon_lane`) is PR #41 and is not
on `main` yet; this branch is merged with it when it lands, and the deletion of
`restart` wins over its edits to that file.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* WIP (#43): the one assertion description in the new section whose BACKTICKS bash ran — `lanes` was executed mid-suite and spliced its own footer into the line a person reads

FOUND BY THE HYGIENE TEST THAT EXISTS FOR IT, and then seen in the suite's own
output. `test_the_suite_descriptions_run_no_command_of_their_own` walks every
`is`, `has`, `hasnt` and `skip` description in `tests/test_lane_helpers.sh` and
refuses an unescaped backtick, because bash reads one as a command
substitution: it was written after the `tests-macos` log of `29d3417` caught
two such lines dying on `kill <pid>` and a third silently RUNNING `forks`.

THE LINE WAS `tests/test_lane_helpers.sh:5662`, in the section Amendment 18
Addendum 1 adds, and it ran `lanes` FOR REAL in the middle of the run. What the
operator saw in this session's own measurement was the description with the
command's output in the middle of it:

    ok   …while no lane of openRepoTools is recorded.

      next free position:  1
      take it:             lane-start openRepoTools 1
      every lane:          lanes --all still carries the closed lane, because that is the READ

— five words of the description survived. The assertion itself passed and could
not have failed: a description is not compared to anything, which is exactly
why the three that came before it sat in the file unnoticed.

    python3 -m pytest tests/test_repo_hygiene.py tests/test_openrepotools_command.py -q
    before: 1 failed, 168 passed
    after:  1 passed (the named test), 123 deselected

Committed as WIP at a coordinator's swap notice, with the shell suite mid-run;
the run that proves the whole section is the next commit's evidence.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* WIP (#43): the container notice reaches `lane <name>`'s bound-elsewhere refusal too, which is the surface that needs it most — a person's OWN lane refused by naming a machine they are sitting at, with nothing on screen to say why

THE NOTICE EXISTED AND ONE OF ITS TWO SURFACES HAD IT. With no
`$LANES_WORKSTATION` inside a container the workstation is the CONTAINER'S id,
so NO row is this workstation's — and the partition then puts every binding of
this host under BOUND ELSEWHERE. `lane`'s listing has printed
`lanes_workstation_why` beneath its rows since it was written, for exactly that
reader; `lane <name>` reaches the SAME refusal without passing through the
listing, and printed nothing. What a person met was

    lane openRepoTools-3 is bound on Eagle, window …, as of 2h ago,
    and this workstation cannot see another machine's tmux …

on Eagle. Measured in this repository on 2026-09-14, in a container with the
seam unset, which is the measurement the notice's own comment already carries.

`go_elsewhere` now asks for it, on stderr because the refusal under it is, and
`ws_notice` is SAID ONCE PER RUN rather than once per surface: the pick reaches
both — the listing prints it under the rows, then the branch the picked row
takes would print it again — and a person who picked a number out of a listing
that had already explained itself does not need the paragraph twice.

THE SENTENCE IS STILL ASKED FOR AND NEVER RESTATED: `lanes_workstation_why` is
the one wording the writers share and this adds no copy of it.

Tests, in the Amendment 18 Addendum 1 section: `lane repoPick-7` — a binding of
THIS host, which is the row an unset seam turns into somebody else's — refuses
with 2, names the workstation its row carries, carries the sentence, names the
launcher whose job the value is, never names the `workstation:` key R-A11-14
rejected, and says it ONCE; and an ordinary elsewhere refusal with the seam
answering carries no notice at all, so the notice means what it says.

Shell suite before this commit, on the two commits below it: 1595 passed,
0 failed, 0 skipped, 0 pending. This commit is WIP at a coordinator's notice
with the re-run still to make; it is made now so the `origin/main` merge that
brings Amendment 15 in lands on a clean tree.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Copilot round 1, all eight taken: a `select-window` that FAILED is a refusal and not an attach onto another lane's window, a binding whose liveness could NOT be read is never launched, `--install` REMOVES the `restart` it once placed, and the flags the two aliases were handed are forwarded or refused rather than dropped

EIGHT FINDINGS, EIGHT TAKEN, NONE DECLINED. Copilot's first review of #45
(2026-09-14, "🟡 Changes recommended", 3 inline and 5 suppressed). Each one
below is a defect THIS pull request introduced, and each is proved by a case
that fails against the commit before it.

## The two it called critical

**`lane:610` — the select was part of the fence and was written as a
courtesy.** All four attach paths put the terminal on the lane's OWN window,
and three of them do something after it. The outside-tmux path spelled that
select `|| :` and then `exec tmux attach`: a window that closed in the half
second between the id-AND-session check and the act left the attach landing on
whatever window that session had current — ANOTHER LANE — which is precisely
what the fence above it exists to stop. One `select_window_or_refuse` now
carries all four sites, naming what had already been done (a client switched, a
window moved) so the refusal says where the person is.

**`lane:777` — an unread session record was being read as a proof.**
`lanes_rows` says on STDERR, and goes on answering, that this workstation's
session records could not be read, in which case its STATE column is *"the
LOG's verb alone: a lane that is live may show as IDLE or PAUSED here"*
(`lanes-edit.sh:4628`). This command relayed that sentence and then grouped the
row AVAILABLE and launched it — a second process on a lane that may still be
running, which is the collision clause (h) is about and the fall-through
Amendment 7(d) forbids. `read_rows` now catches the sentence out of the stderr
it is already reading, and `go_available` refuses on it — for a BINDING alone:
a PAUSED lane is parked by its own last line, needs no liveness proof, and
launches exactly as it did.

## The moderate ones

**`openRepoTools:234` — a word that left the list had not left the directory.**
Dropping `restart` from `INSTALLABLES` stops THIS run from copying it and does
nothing about the copy the last run placed, so every workstation that took the
install before Amendment 18 Addendum 2 would have kept `restart` on its PATH
for ever — against the addendum's own words, *"`restart` is no longer a command
a person is given"*. `--install` now RETIRES it, and removes only what it
wrote: the test is the header this installer puts in every command it owns, so
a person's own script of that name is NAMED, left exactly where it is, and the
`rm` that removes it is printed for them to run.

**`lane:710` — the `f` answer dropped `--dir`.** The flag is in this word's own
parser and `lane-start` takes the same one, so `lane --dir <path>` followed by
`f` opened the new lane in a checkout nobody had named — Evidence 3's silent
loss, at the one moment a lane is created.

**`lane:701` and `lane-start:658` — PATH before the sibling, against this
family's own rule.** `lane` states it fifty lines above its own `$LANES_EDIT`
lookup: *"beside this command first … then on PATH. One resolution, so a
workstation with two copies never reads one and writes the other."* Both new
lookups had it the other way round. `lane-start`'s older `lanes` lookup took
the same correction in the same commit, so the file carries ONE order rather
than two.

**`lane-start:661` — the alias dropped every flag it had been handed.** The
parser takes `--dry-run`, `--no-launch`, `--confirm`, `--yes`, `--dir` and
`--estate` before the zero-positional branch is reached, and the `exec` carried
none of them: `lane-start --no-launch`, a flag whose whole promise is that
nothing starts, became an interactive pick that launches. `--dry-run` and
`--dir` mean the same thing in `lane` and go with it; the rest are REFUSED BY
NAME, with the form that does take them filled in. A flag silently dropped is a
promise that was not kept.

**`lanes:432` — `|| :` collapsed every status of the `next-free` read.** The
footer then told a person to take the install for a read that had FAILED. `2`
is the un-upgraded workstation and still says so; anything else names the code
it got and offers no position (Amendment 7(d)) — and a `0` that comes back with
nothing a position can be made of is in that second class.

## And one this repository's own hygiene caught on top

`grep -q "<pattern>" -- <file>` puts the `--` AFTER an operand, which BSD's
`getopt` reads as a filename; `test_no_shipped_bash_ends_its_options_after_an_operand`
named it in `lane` before any of it ran on macOS. Both new greps take `-e`,
like the line above them that always had.

## Evidence

    python3 -m pytest tests/test_openrepotools_command.py \
                      tests/test_repo_hygiene.py \
                      tests/test_install_skill_and_hook.py -q
      before this commit  197 passed
      after               199 passed   (the two the retirement adds)

Shell suite on the commit BELOW this one, which is this branch merged with
`origin/main` and before any of the above:
  1781 passed, 0 failed, 0 skipped, 0 pending
and for comparison, each a full run out of a clean extract of that commit:
  main 92eab6c (this branch's base)   1500 passed
  main 8345d2f (after #41)            1678 passed
  this branch before the merge        1595 passed
The run that covers this commit is `python3 -m pytest tests -q`, in flight as
this is written; it is committed now at a coordinator's swap notice and the
footer goes in the pull request.

New cases, all in `tests/test_lane_helpers.sh`'s Amendment 18 Addendum 1
section unless said otherwise: a `select-window` that fails on the outside-tmux
path and on the move path (the section's fake tmux grows one knob, and the move
is still in its log because it happened); a binding refused where the liveness
read did not answer, with a PAUSED lane beside it still launching; `f` carrying
its `--dir`, resolved; bare `lane-start` refusing `--no-launch`, naming every
dropped flag rather than the first, and forwarding `--dry-run` through to the
act `lane` then prints instead of performing; `lanes` listing where the
`next-free` read FAILED without calling it an un-upgraded workstation. In
`tests/test_openrepotools_command.py`: `--install` removing a pre-seeded
`restart` that carries this installer's header, and leaving one that does not
while printing the `rm`.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Round 1's own three misses: two refusals printed a command the helper would refuse, the footer named an exit code the read had not given, and the manual still said `--install` only STOPS placing `restart`

THE ROUND THAT FIXED EIGHT THINGS INTRODUCED THREE OF ITS OWN, and every one of
them is the same class of defect this repository already holds itself to: a
refusal is a line a person TYPES, and a line they cannot type is a refusal that
sent them nowhere.

**`lane:553` — `lanes-edit.sh live-holder --lane <lane>` is not a command.**
The liveness refusal added in the commit below this one printed it. `--lane` is
`who`'s spelling (`lanes-edit.sh:who`, whose option loop takes it); `live-holder`
takes a BARE lane — `lane="${1-}"; [ -n "$lane" ] || die "usage: live-holder
<lane>"` — so what was printed would have been read as a lane named `--lane`
and refused by `check_lane_name` for its leading dash. The neighbouring line in
`go_elsewhere` is `lanes-edit.sh who --lane $R_LANE` and IS right, which is how
the wrong one got written.

**`lanes:479` — the reproduction piped the WRONG rows in.** `next-free` reads
`lanes-edit.sh lanes`'s tab-separated rows on stdin, not this command's
rendering of them, so `lanes --prefix <repo> | lanes-edit.sh next-free <repo>`
would have handed the helper a listing it cannot parse and produced a second
confusing answer on top of the first. It is the helper's own listing now:
`lanes-edit.sh lanes --prefix <repo> | lanes-edit.sh next-free <repo>`.

**`lanes:434` — the footer claimed an exit code the read had not given.** The
new branch turned a `0` that came back with nothing a position could be made of
into `next_rc=1` and then printed "exited 1" about a read that had exited 0.
The reassignment is gone and the sentence says what actually happened —
`exited <code> and gave no position` — with the 0-with-nothing case named in
the same words rather than disguised as a failure.

**And the manual now says what `--install` DOES.** `docs/README-lanes.md`'s
Addendum 2 paragraph still read "no longer places `restart`", which was the
whole truth before the round and is the smaller half of it after: the installer
REMOVES the copy an earlier install placed, printing `restart: RETIRED` beside
the file it took away, and removes ONLY what it wrote — a file of that name
carrying this installer's own header goes, one that does not is NAMED, left
exactly as it is, and the `rm` printed for the person to run.

Measured by hand on this branch, both branches of it, `--install` run out of
the checkout into a temporary HOME:

    restart: RETIRED, removed from …/.local/bin/restart (lane-collision-protocol
      Amendment 18 Addendum 2; `lane <name>` is that act now)
    $ ls …/.local/bin/restart → No such file or directory

    restart: RETIRED by lane-collision-protocol Amendment 18 Addendum 2, and
      …/instest2/restart is NOT this installer's copy — left exactly as it is.
      Remove it yourself if it is stale:  rm …/instest2/restart
    $ cat …/instest2/restart → the stranger's script, byte for byte

and in both runs the `/restart` SKILL is placed exactly as before, at both of
its two paths — Addendum 2 clause (i-7): what left the PATH is the OUTSIDE half,
and `/restart` was always the inside one.

No assertion changes and none is weakened: the two texts are named by their
distinctive words (`live-holder`, `exited`) and both still carry them. No cap
moves — `README.md` 409 (cap 409), `AGENTS.md` 224 (cap 224), both untouched;
`docs/README-lanes.md` has no cap and takes five lines.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Copilot round 2 on round 1, all four taken — the retirement no longer follows a SYMLINK a person made, the select's target names its session, a moved window's refusal names where it IS and not where it came from, and macOS's `/var` is why `tests-macos` was the only red

FOUR FINDINGS ON MY OWN FIX ROUND, AND ONE ON THE PREDECESSOR'S, and the first
of them could have deleted something somebody owns.

## Copilot round 2 (on `eb10256`), four findings, four taken

**`openRepoTools:262` — `-f` FOLLOWS THE LINK.** The retirement's ownership
test was `[ -f "$target" ] && grep -qF …`: a person's own `restart` SYMLINK
pointing at a file that happens to carry this installer's header would have
been UNLINKED by it. `plan_install_targets` refuses a `-L` target for exactly
that reason — `unplaceable_kind` names the link rather than writing through it
— and the retirement now holds to the same rule. `[ ! -L "$target" ]` first, so
a link is NAMED, left, and the `rm` printed; both it and what it points at are
still there afterwards, which is the new case.

**`lane:666` — the select's target was an `@id` alone.** tmux resolves a bare
`@id` by picking the "best" session holding that window, and PICKING is the one
thing this command's id-AND-session fence exists to stop. Every select is
`<session>:<@id>` now — in the acts and in the `--dry-run` lines that print
them — so the act is the one the record described or it is nothing.

**`lane:706` — after the move, the refusal named the SOURCE.** `move-window -a
-s <src>:<idx>` puts the window in THIS session and a detached source with no
windows left closes itself; the refusal under it still said "in session <src>"
and offered `tmux attach -t <src>`, which is a session that may be gone.
`select_window_or_refuse` takes the session it means, and after a move that is
`$gl_this`.

**`lane:589` — the no-directory recovery dropped the profile that was typed.**
`lane <name> <profile>` on a lane whose record has a profile but no directory
passes the profile check and lands on the directory refusal, whose one-run exit
was `lane --dir <path> <name>` — following which walks straight back into the
NO-PROFILE refusal. It carries `${profile:+ $profile}` now.

## And the one CI found that no review did

`tests-macos` on `fce18d5` was the only red job: **1764 passed, 2 failed**, both
in the predecessor's `--dir` case —

    FAIL …launching it under the profile that was typed, with the directory named
    FAIL …from that directory

macOS's `$TMPDIR` is under `/var`, a symlink to `/private/var`. `lane` resolves
`--dir` ONCE, where the operator typed it, because it `cd`s and then hands the
same string to a launcher that would resolve it a second time from somewhere
else — so the launcher sees `/private/var/...` while the case asserted the
sandbox's own `/var/...`. Asserting the unresolved spelling is asserting that
the resolution did NOT happen, which is the one thing that flag must do. The
suite already had `$A11_DIR_R` for the relative-`--dir` case fifty lines down;
it is computed ONCE now, beside `$A11_DIR` itself and AFTER the `mkdir` that
creates it — a `cd` into a directory that does not exist yet falls back to the
unresolved spelling and is the same defect written twice.

The other three CI jobs on that commit were green: `tests`, `tests-no-submodule`
and `tests-windows`.

## Evidence

    python3 -m pytest tests -q   on the round-1 tree: 573 passed in 1406s
    (the shell suite inside it: 1781 passed, 0 failed, 0 skipped, 0 pending)

and for the record, each a full run out of a clean extract of that commit:

    main 92eab6c (this branch's base)   1500 passed
    main 8345d2f (after #41)            1678 passed
    this branch before the merge        1595 passed

New cases: the select assertions take the session-qualified target; the move's
refusal is asserted to name the session the window is in NOW and never to send
a person back to the source; and `--install` is run against a SYMLINK of a
retired name, which must survive with what it points at. The run covering this
commit is `python3 -m pytest tests -q`, in flight as this is written.

A NOTE ON HOW THESE RUNS WERE SEQUENCED. The concurrency guard this work was
briefed with — `while pgrep -af 'python3 -m pytest' | grep -v pgrep >/dev/null`
— is a NO-OP in this harness: `grep` is a shell function from the session's zsh
snapshot and returns 1 with stdout on `/dev/null` even when it matched, so the
loop never waits. Measured: the same pipeline captured into a variable gives
rc=0 and five matching lines. Four agents were running `pytest` at once past
that guard. The form that works splits the pattern so it cannot match the
guard's own command line:
    pat='python3 -m pyt'"est"; while [ "$(pgrep -f "$pat" | wc -l)" -gt 0 ]; do sleep 20; done

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Copilot round 3, all four taken — three states where the liveness proof had two, the `f` answer no longer vanishes from the question without a word, and a position printed by a read that then FAILED is not a position

FOUR FINDINGS, FOUR TAKEN, and three of them are the SAME defect the round
before fixed one file over: a read whose status was thrown away with `|| :` and
whose emptiness was then read as an answer. Amendment 7(d) is the rule and it
is one rule; taking it in `lanes` and leaving it in `lane` was taking half.

**`lane:960` — the `f` answer vanished from the question with nothing on
screen.** The picker's `next-free` read spent `|| :` on every status, so on an
un-upgraded workstation — or a read that broke — the listing simply lost its
new-lane action and the question narrowed to `[1-N, q]` with no explanation at
all. It now says which: `2` is a `lanes-edit.sh` predating Addendum 1 and names
the install; anything else names the code it got and the read to run by hand.
Neither is a position of 1.

**`lane:751` — `f`'s own read discarded its status too, and that one ALLOCATES.**
A `next-free` that printed digits and then failed handed those digits straight
past the numeric fence below it and into `lane-start`, which is a NEW LANE
opened at a position this estate may well have taken. The status is read first
now and any non-zero is a refusal naming it.

**`lane:322` — THREE STATES, because "I could not read whether the proof was
made" is not "the proof was made".** With no writable `$TMPDIR` the helper's
notes go straight to the terminal and this command cannot read them back, and
`LIVENESS_UNREAD` stayed `0` — so a binding whose liveness may never have been
established was launched anyway, which is the collision clause (h) exists for
and exactly the fall-through Amendment 7(d) forbids. `0` the proof was made ·
`1` the helper SAID it could not make it · `2` this command could not read
whether it did; `go_available` refuses on `1` and `2` alike, in each one's own
words, and a PAUSED lane — parked by its own last line — still launches under
every one of them.

**`tests/test_repo_hygiene.py:34` — the inventory's own description said
"four".** `SHIPPED_BASH` has held six since this branch added `lane` beside
`lanes`, and the paragraph above it still told a reader the macOS job parses
four. A description that contradicts the list it explains is the drift these
comments exist to prevent.

New cases, all in the Amendment 18 Addendum 1 section: the pick listing where
`next-free` is MISSING and where it FAILED, each saying why `f` is not offered
and never offering a position; `f` answered on a failed read, refusing with 1
and starting nothing; and a binding refused under a `$TMPDIR` that cannot hold
a temporary file, with a PAUSED lane launching beside it. The two `next-free`
wrappers move up to the fixtures so the pick's cases and the listing's cases
read the same two fakes rather than two spellings of them.

Measured before this commit, and the run that covers it is
`flock /tmp/claude-1000/openrepotools-pytest.lock python3 -m pytest tests -q`,
in flight as this is written:

    python3 -m pytest tests -q   round-1 tree: 573 passed, 0 failed (1406 s)
      the shell suite inside it: 1781 passed, 0 failed, 0 skipped, 0 pending
    main 92eab6c   1500 passed · main 8345d2f (after #41)   1678 passed
    this branch before the merge with #41                   1595 passed

CI on `c42d962`: `tests`, `tests-no-submodule` and `tests-windows` green,
`tests-macos` still running at the time of writing — the macOS-only `--dir`
defect that made it red on `fce18d5` was fixed in `f00229d`, one commit later.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* CI's one red case and Copilot round 4, six taken and two declined — a PAUSED row is the one an unread liveness proof is LEAST safe to launch, the move is fenced by the `@id` it checked instead of an index another window can be given, and an `-e` that skipped a DANGLING `restart` link left it on PATH

THE RED FIRST. `tests` and `tests-no-submodule` were red on `9581ac9` with ONE
case between them — `1832 passed, 1 failed` on both — and it is the round-3
commit's own prose reflow: pulling `$ga_why` out as a variable re-wrapped the
refusal so that `PROVED` ended one line and `that binding dead` began the next,
which the assertion reads as a substring of one line. The words are the fix, not
the assertion. (`tests-windows` was green on that commit; `tests-macos` had not
finished.)

**`lane:560` — THE `PAUSED` EXCEPTION WAS A HOLE IN THE FENCE AT ITS WIDEST
POINT.** Round 1 made an unread liveness read refuse a launch and let `PAUSED`
through, on the reading that a lane parked by its own last line needs no proof.
The sentence that refusal is REACTING to says otherwise, and this file already
quoted it whole: *"a lane that is live may show as IDLE or PAUSED here"*
(`lanes-edit.sh:4628`). The helper's own state machine is the second half —
*"a live record naming one of the lane's ids beats the log's own last verb …
otherwise the verb answers"* (`lanes-edit.sh:4700`) — so when the live read
fails the verb is ALL that is left, and `/lane-swap` writes the `PAUSED` line
while its session is still on screen. The row most likely to be running was the
one row passed through. Every state refuses now; the two cases that asserted the
launch assert the refusal, in each unread state's own words.

**`lane:715` — THE MOVE IS SPELLED WITH THE `@id`, AND MEASURED.** The source
was `<session>:<index>`, read one command before it was used. tmux 3.4 on a
private socket, 2026-09-14: with `w3` at `src:2`, killing it, opening another
window at that index and then `move-window -a -s src:2` MOVED THE INTRUDER into
the destination and exited 0 — the wrong window, in this session, silently —
while `move-window -a -s src:@2` refused with `can't find window: @2` and exit 1.
The `@id` is what the fence above checked, tmux resolves it inside the named
session or not at all, and the move is now a checked act: a move that failed is
a refusal and the select under it never runs. The index read and its
"tmux gave it no index" refusal are gone with it.

**`lane:687` — UNDER `--switch`, THE SELECT COMES FIRST.** `switch-client` then
`select-window` leaves a client that lost the race sitting in the target
session's current window, which is another lane and this terminal already in it.
`select-window` names the session it acts on and needs no client there, so the
lane's own window is made current BEFORE the client moves; a select that fails
now costs nothing, and a `switch-client` that fails is a refusal naming the half
that did happen.

**`lane:595` — A LANE WHOSE NAME IS NOT `<repo>-<n>` IS STARTED BY ONE
ARGUMENT.** The no-directory refusal built its remedy as `<repo> <position>`,
and a `--verbatim` lane has neither: `lane-start --dir <path> browser-ui-repair
<n>` opens `browser-ui-repair-<n>`, A DIFFERENT LANE. `lane-start:601` takes
that name as one argument beside a `--dir`, which is what the line prints now.

**`lane-start:690` — THE VALUE GOES WITH THE FLAG THAT TOOK ONE.** `lane-start
--estate x --yes` printed `lane-start <repo> <n> --estate --yes`, whose
`--estate` swallows the `--yes` and names an estate `--yes`. The `--` words were
dropped the same way. A line printed for a person to type is a line this command
has to be willing to have typed back at it.

**`openRepoTools:261` — `-e` FOLLOWS THE LINK AND IS FALSE FOR A DANGLING ONE.**
A `restart` SYMLINK whose target has since gone — and the pre-move
`link-estates` put exactly those links in a workstation's `~/.local/bin` — was
neither removed nor named by the retirement: it stayed on PATH and the one run
that was going to mention it said nothing. `-L` is the other half of "is there
anything at this path"; what happens next is what happens to any link, which is
that it is named and left.

TWO DECLINED, BOTH MEASURED RATHER THAN ARGUED, because a decline that is only
argued is one the next round makes again (the pattern this suite already uses
for `ls --`):

* **`tests/test_lane_helpers.sh:6424` — `env -C` is not a GNU-only option
  here.** The macOS job has run these very cases GREEN: job 104128188482 on
  `fce18d5` printed `ok a bare lane inside a checkout exits 0` and `ok answering
  with the number of an AVAILABLE lane exits 0`, both of them `env -C` forms,
  neither of which can exit 0 and carry a listing if `env` had refused the flag
  — and `main` has carried four more since Amendment 11. Asserted directly now,
  on every platform the suite runs on.
* **`openRepoTools:263` — the header marker is the only ownership evidence an
  installer can have about a file it wrote BEFORE it knew it would retire it.**
  Durable metadata cannot answer for the population this act exists for: no
  installer that placed `restart` wrote a receipt, so no receipt can identify
  its output. The marker is this installer's own sentence, naming itself, at
  line 12 of the file it placed; the `! -L` fence (round 2) and the `-L` guard
  above keep it off anything that is not a regular file it can read. The receipt
  idea is real for FUTURE retirements and is filed as opensoft/openRepoTools#57.

And three of round 4's suppressed comments — `lane:765`, `lane:969` and
`lane:322` — were already fixed in `9581ac9`, one commit after the tree it
reviewed.

New cases, all in the Amendment 18 Addendum 1 section: a `move-window` that
FAILED refusing with nothing selected; `--switch` with a select that failed
never reaching `switch-client`, and a `switch-client` that failed refusing after
the select that worked; a `--verbatim` lane's no-directory refusal naming the
one-argument form and never a position; the `--estate` value in the alias
refusal; `env -C` measured on this platform; and in
`tests/test_openrepotools_command.py`, an install run against a DANGLING
`restart` symlink, which survives and is named. The fake tmux learns the two
acts that can fail after the fence has passed.

Measured: `bash tests/test_lane_helpers.sh` — 1846 passed, 9 failed, where all
nine are a concurrent writer on this workstation deleting the run's `$SANDBOX`
and SIGTERMing its liveness fixture mid-run (`expected [0], got [143]`, then
every read of a path under it): the last section and the two sentinel checks,
none of them a case this commit touches, and every case of this section passed.
The `python3 -m pytest tests -q` run under the workstation's lock is in flight
as this is written; CI is the same command.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m

* Copilot round 5, three taken and six filed — a `next-free` that printed digits and THEN failed was still offering them one surface up from where round 3 stopped it, the acts a no-terminal run PRINTS dropped the `--dir` the `f` answer has carried since round 1, and the AVAILABLE heading now says what an unread liveness proof costs it

THE SEEDED LANE WAS ALREADY SEEDED, which is what CI found on `f6200a05`:
`1854 passed, 1 failed`, the one failure this branch's own new case. Line 373
has carried a `browser-ui-repair` row since long before this section, and line
808 RETIRES it — so a second row of that name is two rows differing only by
case, which is `canon_lane`'s 2 (Amendment 15) and the refusal it prints rather
than the one the case was written for. The `--verbatim` lane is `nav-rail-audit`
now, a name this suite does not otherwise use, and the case reads the same.

**`lane:1000` and `lanes:441` — A READ THAT PRINTED DIGITS AND THEN FAILED IS
STILL A READ THAT FAILED.** Round 3 closed this in the `f` ANSWER (`go_new`,
`lane:751`) and left it open in the two surfaces that OFFER `f`: both captured
the status into a variable and then decided on the TEXT —

    next_free="$(… next-free …)" || next_free_rc=$?
    case "${next_free:-}" in '' | *[!0-9]*) next_free="" ;; esac

— and text printed by a read that then failed passes a shape test on its own
output. The fail-closed arm below was skipped for exactly the case it exists
for, so `lane` offered `f` at a position nothing answered for and `lanes`'
footer printed `take it: lane-start <repo> <that position>`. The status decides
before the text does now, in both files, and the case that proves it is a fake
that prints a position and exits 6 — `nfbroke` prints nothing, which is why
neither surface had ever met this.

**`lane:1030` — THE ACTS A NO-TERMINAL RUN PRINTS CARRY THE `--dir` IT WAS
GIVEN.** The `f` answer has carried it since round 1 (`lane:710`); the line
PRINTED for an agent to type dropped it, which opens the new lane in whatever
checkout the reader happens to be standing in — Evidence 3's silent loss, at the
one moment a lane is created. One surface performed the act and the other
printed it; they spell it the same way now, `lane <name>` included.

**`lanes-edit.sh:4829` — THE `AVAILABLE` HEADING SAYS WHAT AN UNREAD PROOF
COSTS.** The partition is right and is one read: `lane-groups` groups a local row
that is not LIVE as available, out of the STATE column — which is the log's verb
alone when the session records could not be read. With the `PAUSED` exception
gone (round 4, `lane:560`), EVERY row under that heading is then one
`go_available` refuses, and a listing that numbered them without a word would be
asking a person to pick an answer this command has already decided to refuse.
The paragraph is printed under the heading it is about, names `live-holder` and
`lanes --all`, and says that a LIVE HERE attach is unaffected because an attach
starts nothing. It is silent where the proof was made.

AND THE CODE STAYS ON THE WORD IT BELONGS TO: `lanes`' own footer wrapped
between `exited` and the number, so a refusal whose whole job is to say WHICH
code it got did not say it to anything reading a line at a time. That is the
same defect as CI's red on `9581ac9`, where a reflow split `PROVED that binding
dead` across two lines.

SIX MORE OF ROUND 5's THIRTEEN ARE PROSE THAT SAYS "four" about inventories
holding six, ten and eleven — `README.md:333` and `:403`,
`.github/workflows/tests.yml:177`, `openRepoTools:370`,
`tests/test_repo_hygiene.py:6` and `:2126`,
`tests/test_openrepotools_command.py:9-11` and four more docstrings,
`tests/test_lane_helpers.sh:30` and `:312`. Every one of them is ALREADY FALSE
on `main` at `8345d2f`, where `INSTALLABLES` holds eleven and `SHIPPED_BASH`
six: this branch renames one member of those lists and moves no count. Filed as
opensoft/openRepoTools#58 and claimed by this lane, rather than folded into a
diff that is already fourteen files. Two more of the thirteen restate findings
this branch took in `f6200a05` (`lane:728`, `openRepoTools:261`).

Measured, this tree, under no contention from the other suites this workstation
was running:

    bash tests/test_lane_helpers.sh   1874 passed, 0 failed, 0 skipped, 0 pending

CI on `f6200a05`: `tests` and `tests-no-submodule` red on the one seeding case
above (`1854 passed, 1 failed`), `tests-windows` green, `tests-macos` still
running at the time of writing. The `python3 -m pytest tests -q` run under this
workstation's lock is in flight.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m

* Copilot round 6, one taken and one declined — the outside-tmux attach names the WINDOW and not only the session, so no act between the fence and the terminal can land on another lane, and an empty listing's `1` is measured to be the helper's own answer

**`lane:708` — A SELECT AND AN ATTACH ARE TWO ACTS.** Outside tmux this word
selected the lane's window and then ran `tmux attach -t <session>`, and a window
that closed in between left the attach on whatever that session had since made
current — another lane, which is the one thing the id-AND-session fence exists
to stop. It is the same defect round 1 took in the select and round 4 took in
the move, one act further along.

MEASURED, tmux 3.4 on a private socket, 2026-09-14, with `dst` current on `@0`:

    attach -t dst:@2   attached AND made `@2` current, in ONE act
    attach -t dst:@3   (a window of another session)  can't find window: @3

So the attach names `<session>:<@id>` now, in the act and in the `--dry-run`
line that prints it. The select above it stays: it is this command's own
refusal, in this command's words, while there is still a terminal to print it
on — and the attach then resolves the same target at the moment it acts.

**`lanes:424` — DECLINED, and measured rather than argued.** The `rc=8` branch
prints `next free position: 1` without calling `next-free`, which round 6 read
as a position "already taken" on a workstation whose helper predates that read.
It cannot be: `lane_next_free` (`lanes-edit.sh:4847`) reads ONLY THE ROWS ON
STDIN and keeps those whose name begins `<repo>-`, and the rows it would be
handed are the ones that listing just found NONE of — while `lanes_rows`' own
`--prefix` filter keeps every lane whose NAME carries the prefix (its third
or-term). An empty filtered listing IS "no lane named `<repo>-<n>` in this
register", and 1 is the only answer the helper could give. Routing it through
that helper would add a FAILURE PATH in front of an answer that needs no read:
an un-upgraded workstation would be refused the one position it can be certain
of. Four assertions now hold both halves together — the listing's 8, its `1`,
and `next-free` answering `1` over the same empty rows, twice.

THE REST OF ROUND 6 IS THE TREE IT READ. It reviewed `f6200a05`, one commit
behind: `lane:1048` and `lanes:440` (a `next-free` that printed digits and then
failed) were taken in `39ec308`, and `lanes-edit.sh:4829` was answered there
too — with a PARAGRAPH UNDER THE HEADING rather than a change to the grouping,
deliberately. The partition is ONE read that `lane` and `lanes` share; moving a
row out of AVAILABLE while the liveness is unknown would either hide a lane from
the person who most needs to see it or invent a fourth group in a read two words
render. What is true is that every row under that heading is one `go_available`
will refuse, and the heading says exactly that, names `live-holder` and
`lanes --all`, and says a LIVE HERE attach is unaffected.

And five of its nine are the "four" prose filed as opensoft/openRepoTools#58 and
claimed by this lane: `README.md:333` and `:403`, `openRepoTools:370`,
`tests/test_openrepotools_command.py`'s module and fixture docstrings, and
`tests/test_repo_hygiene.py:6`. Every one is already false on `main` at
`8345d2f`, where `INSTALLABLES` holds eleven and `SHIPPED_BASH` six.

Measured, this tree:

    bash tests/test_lane_helpers.sh   1879 passed, 0 failed, 0 skipped, 0 pending

and the `python3 -m pytest tests -q` run under this workstation's lock covers
this same tree. CI on `39ec308` was in flight as this was written; the run
before it, on `f6200a05`, was red on one case only — the seeding collision
`39ec308` corrects — with `tests-windows` green.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m

* Copilot round 7, five taken — the switch names the WINDOW like the attach now does, a position a lane has held is RESERVED and the comment that said otherwise was the drift, `repo-foo-1` is no lane of `repo`, a printed `--estate` value is quoted, and the one destructive step of `--install` is its LAST

**`lane:738` — THE SWITCH NAMES THE WINDOW, for the reason the attach does.**
Another client can make that session's current window something else between
the select and the switch, and `switch-client -t <session>` would then land this
client on it. MEASURED, tmux 3.4 on a private socket, 2026-09-14, with a client
on `home` and `dst` current on `@1`:

    switch-client -t dst:@3     client moved to `dst` AND `@3` made current, ONE act
    switch-client -t dst:@999   can't find window: @999   (exit 1)

**`lanes-edit.sh:4839` — A POSITION A LANE HAS HELD IS RESERVED, and the
paragraph claiming otherwise was the drift.** It said positions "come back as
lanes end"; `lane_next_free` has never done that — every row on stdin is taken,
whatever its state, and the fixture proves it (`repoPick-5` ENDED, next free 6).
The CODE is right and the comment was wrong: a lane's identity is its name, its
object log `lanes/log/<lane>.md` is APPEND-ONLY, and a second lane at a retired
position would write its life into the first one's file — so every read of that
log would answer for two lanes at once. Positions are cheap; identities are not.
The manual's reads table says it too, where it describes what that read answers.

**`lanes-edit.sh:4853` — `repo-foo-1` IS NO LANE OF `repo`.** The prefix test
was `substr(l, 1, length(rl))` against `<repo>-`, so a repository whose name is
another name plus a hyphen took positions out of its neighbour: `next-free repo`
would skip 1 because `repo-foo-1` exists, and hand out a number for a reason
nobody could see in the listing. The name before the LAST hyphen is compared
whole now, case-insensitively, which is the same rule the rest of this file
reads lane names by (Amendment 15).

**`lane-start:698` — A PRINTED VALUE IS QUOTED THE WAY THE SHELL READS IT
BACK.** `--estate 'team blue'` printed bare is two words when it is typed again
— a suggested command that does something else — and the `--` words had the
same problem. Both go through `quoted`, which is the helper every other printed
command in that file already uses. (The value being there at all is round 4's
`lane-start:690`; this is its second half.)

**`openRepoTools:369` — THE ONE DESTRUCTIVE STEP IS THE LAST ONE.**
`retire_commands` ran BEFORE `place_skill_and_hook`, so a skills directory or a
`settings.json` this run could not write left a workstation with the old word
already DELETED and the adoption it was migrating to unfinished. Every other
refusal in that file places nothing rather than half of it; this one now removes
nothing until the rest of the install has actually happened. The case makes
`~/.claude/skills` a regular file — a `mkdir -p` that cannot succeed — and
requires the stale `restart` to still be there afterwards, with no `RETIRED`
line claiming otherwise.

New cases: the `--switch` log carrying `switch-client -t detsess:@32`; an
`--estate` value with a space in it, quoted and never bare; `next-free` over a
neighbour repository's rows (`repoNb-foo-1`, `repoNb-foo-2` → 1) and over its
own under the same names (→ 2); the ENDED position asserted as RESERVED in those
terms; and `test_install_retires_only_after_the_placement_it_migrates_to`.

Measured, this tree:

    python3 -m pytest tests/test_openrepotools_command.py -q -k 'retire or dangling or migrat'   3 passed

and the full `bash tests/test_lane_helpers.sh` footer is on the MERGE COMMIT
above this one, because `origin/main` moved under this branch while these five
were being taken (#44, the installer) and the run worth reporting is the run on
the tree that lands.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m

* Copilot round 8, three taken and two declined — every act this word PRINTS is now a line that can be typed back: the `cd` is quoted, `--switch` survives into it, and a bare `lane-start`'s remedy carries the flag it KEPT and not only the one it refused

Three surfaces print a command for a person or an agent to run, and each dropped
something the run it describes would need.

**`lane:612` — THE `cd` IS QUOTED WHERE IT IS A PATH.** The no-profile refusal
hands over one line — `cd <the lane's dir> && pclaude --lane <lane> <profile>` —
and a recorded directory with a space in it made that `cd` two arguments. `%q`
through `quoted`, the same helper the dry-run uses; the PLACEHOLDER arm is left
as words, because `\<the\ lane\'s\ checkout\>` is not a thing to type either.

**`lane:1123` — `--switch` IS A DIFFERENT ATTACH AND NOT A PREFERENCE.** The
no-terminal listing printed `lane <name>` with the flag dropped, and typing that
back MOVES a live lane's window into the current session instead of switching
the client to it — a different act on somebody's terminal. It goes on the `lane`
line alone: `lane-start` has no such flag.

**`lane-start:706` — THE REMEDY CARRIES WHAT IT KEPT.** It was built out of the
REFUSED flags alone, so `lane-start --dir /work/repo --estate x` printed
`lane-start <repo> <n> --estate x` and the checkout the person had named was
gone from the line they were told to type. `--dry-run` went the same way. What
is suggested is the whole command they meant.

TWO DECLINED:

* **`lane:721` — the outside-tmux attach `exec`s, and that is the contract.**
  This word is `exec`ed by the launcher and an attach must REPLACE this process
  rather than leave a shell waiting under the tmux client for the life of the
  session. What is left after round 6's fix is a window that vanishes between
  `select_window_or_refuse` — this command's own refusal, in its own words, at
  exit 2 — and the `exec` microseconds later, where tmux refuses the qualified
  target itself: `can't find window: @32`, exit 1. That is the fence doing its
  work and saying so, not an unexplained read failure; translating it would mean
  not replacing the process, which costs more than the code it would buy.
* **`lane:944` — the population does not exist.** The finding is that an older
  `lanes-edit.sh` would reject `read_rows --lane` with 64 and die before the
  estate-wide fallback under `canon-lane`'s 2. Measured: `lanes)` and its
  `--lane)` arm landed in ONE commit, `63a74af` ("Lane tooling: restart,
  /restart, lanes, session-lane, window-lane, dir and profile in the record"),
  so every helper that has the `lanes` subcommand takes `--lane` — and one that
  does NOT have it returns the contract's 2 from `read_rows`, which is the
  "predates Amendment 11, take the install" refusal and is intended. There is no
  version in this repository's history between those two states.

AND ROUND 8's `lane-start:698` IS ROUND 7's, TAKEN in `2c41f71` one commit
before it read the tree.

New cases: a lane whose recorded directory has a space in it (`repoSpace-1`),
whose no-profile refusal is asserted to carry `cd $(printf %q)` and never the
bare path; a no-terminal `lane --switch` listing carrying `lane --switch <name>`
and never `lane-start --switch`; and a bare `lane-start --dir <path> --estate x`
whose refusal names the refused flag AND the checkout it kept.

Measured, on the merged tree (`origin/main` at a954435 is merged in):

    bash tests/test_lane_helpers.sh   1926 passed, 0 failed, 0 skipped, 0 pending

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m

* Copilot round 9, one taken and two declined — the retirement's printed `rm` is quoted the way both planners in that file already print theirs, and a `PAUSED` lane of another workstation is AVAILABLE here because that is the handoff this protocol is built on

**`openRepoTools:281` — A PRINTED COMMAND IS A LINE SOMEBODY TYPES BACK.**
`OPENREPOTOOLS_BIN_DIR` is a path a person chooses, and the retirement printed
`rm <path>` bare — so a directory with a space in it produced a line that
removes nothing, or removes something else. It is printed the way both planners
in this same file already print theirs, which build their `paths` with the
quotes IN: `rm -f -- "<path>"`, with `-f` for the reason those two give above
their own line — what a removal needs is the DIRECTORY's write bit, not the
file's. The three existing retirement cases assert that spelling now, and a new
one runs the install with `OPENREPOTOOLS_BIN_DIR` set to `<tmp>/my bin dir`.

**`lanes-edit.sh:4870` — DECLINED: a `PAUSED` lane is available on ANY
workstation, and that is the act.** The finding is that `lane_groups` files a
`PAUSED` row from Raven as AVAILABLE on Eagle rather than BOUND ELSEWHERE. It
does, deliberately, and this pull request's own refusal says so to the person
reading it: *"the exit is a person on <workstation> parking it — `/lane-swap` in
that session — after which it is PARKED and `lane <lane>` takes it here"*
(`go_elsewhere`). Routing a parked row to `elsewhere` would refuse the one case
Addendum 2 retired `restart` for, and no parked lane could ever be taken to
another machine. The fences that matter are all still in front of it:
`go_available` refuses in EVERY state where this host's liveness read did not
answer (round 4, `lane:560`); the recorded DIRECTORY must exist here by name,
and the refusal says a lane parked elsewhere records that machine's path and
that `resume <Name>` is what brings the work here; and a lane LIVE anywhere this
host can see is `live` and never `available`.

**`lane:1032` — DECLINED: `rg_list` carries TWO prepended fields.** The finding
reads the loop's line as `lane-groups`' own output and calls the second
`${rg_row#*$TAB}` a lane-name field being eaten. The line is this function's own
list, built four lines above the read as `<repo> TAB <seq> TAB <the whole row>`
— the two keys `sort -t"$TAB" -k1,1 -k2,2n` needs when no one repository is in
scope — so two strips is exactly right. The suite holds it from both ends and
would go red on that shift: the listing asserts `1  repoPick-1` with its STATE,
profile and directory columns, and every numbered answer is asserted through to
what it dispatched (`argv=--lane repoPick-1 team-05a`, `select-window -t
picksess:@31`, `move-window -a -s detsess:@32`). All four CI jobs are green on
`619c484`.

Measured:

    python3 -m pytest tests/test_openrepotools_command.py \
      tests/test_repo_hygiene.py tests/test_install_skill_and_hook.py -q
        230 passed

The shell suite is untouched by this commit (1926 passed on the tree it sits on,
`e8aa021`), and the full `python3 -m pytest tests -q` under this workstation's
lock covers both.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m

* Copilot round 10: the one thing it asked for that this branch did not already have is the CASE that makes round 9's decline durable — a `PAUSED` row of another workstation is AVAILABLE here, asserted so no reordering of that awk can take it away quietly

Round 10 read `e8aa021`: three suppressed comments, no threads, and two of the
three are round 9's `lanes-edit.sh:4870` twice more — that a `PAUSED` row
recorded on Raven is grouped `available` on Eagle. It is, deliberately: that is
clause (c)'s handoff and this pull request's own refusal says so to the person
reading it, *"the exit is a person on <workstation> parking it — `/lane-swap` in
that session — after which it is PARKED and `lane <lane>` takes it here"*.
Declined on the thread, twice; and the third comment is the useful half of the
same reading — **there was no case for it**, so a future reordering of those
four lines would have turned the decline into a defect with nothing on screen.
There is one now, fed straight to the helper:

    a PAUSED row of ANOTHER workstation is AVAILABLE   available
    …while a BINDING of that same workstation is        elsewhere

Synthetic rows rather than a sixth seeded lane, because the six this section
seeds ARE the partition every numbered pick below is counted against, and a
seventh would renumber all of them.

MEASURED BY HAND on this tree before the case was written, which is the same
read the case makes:

    printf 'someRepo-9\tPAUSED\tRaven\n' | ./lanes-edit.sh lane-groups Eagle → available
    printf 'someRepo-9\tIDLE\tRaven\n'   | ./lanes-edit.sh lane-groups Eagle → elsewhere
    printf 'someRepo-9\tPAUSED\tEagle\n' | ./lanes-edit.sh lane-groups Eagle → available

AND ROUND 10's `lane:928` IS COVERAGE THIS BRANCH ALREADY HAS, one section up:
`lane <name>` on an AVAILABLE lane is asserted through to the launcher's argv
AND its cwd — "lane exits 0 for Evidence 7's lane once its directory is
recorded" (`cwd=$HOME/projects/xFactory/xFactories/OpsxFactory`,
`argv=--lane opsXfactory-5 team-03l`), with `lane <name> <profile>` beside it
handing the launcher the profile that was NAMED and not the one recorded. The
Amendment 18 Addendum 1 section is the pick and the three branches; the named
AVAILABLE launch is `restart`'s own case, moved with the act under Addendum 2
and still asserted where it was written.

The shell suite is 2095 passed, 0 failed on the tree this sits on (`e066be1`,
`origin/main` merged at 2faa883); these two assertions make it 2097 and CI's own
`tests` job measures them.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m

* Copilot round 11, two taken — the fence's own refusals offered `tmux attach -t <session>`, which is the wrong-lane outcome they exist to prevent, and a `canon-lane` read that FAILED was read as "the typed spelling will do"

**`lane:499` and `lane:804` — A REFUSAL THAT OFFERS THE BARE SESSION OFFERS THE
DEFECT.** Both ended with `tmux attach -t <session>`, which lands on whatever
that session has since made current — another lane, which is the one thing the
id-AND-session fence exists to stop. The window goes first now and the bare
session stays under it, said as what it is:

      what the register says now:            lanes --all
      that window, if it is back:            tmux attach -t detsess:@32
      that session, whatever it now holds:   tmux attach -t detsess

The qualified form is either the lane or a refusal, measured on tmux 3.4 at
round 6: `attach -t dst:@2` attached AND made `@2` current in one act, and
`attach -t dst:@3` for a window of another session refused with `can't find
window: @3`. A person who wants the session anyway is still told how — knowing,
from the words beside it, what they will get.

**`lane:956` — TWO STATUSES ARE ANSWERS AND THE REST ARE A REFUSAL.** `0` is the
row's own spelling; `2` WITHOUT the 15(d) words is the unknown-subcommand code
of a helper predating Amendment 15, which is the documented fall-through to the
estate-wide read below (that read matches case-insensitively, which is what
makes the fallback work); `2` WITH them is `canon-lane`'s own two-rows refusal,
relayed one branch up. Everything else was falling through to the TYPED
spelling, which is a lane bound under a name nothing established — the defect
Amendment 15 exists to prevent, and the fall-through Amendment 7(d) forbids. It
refuses with 1 now, naming the code and the read to run by hand. `canon-lane`
was the last read in this file without that rule; the row read, the grouping
read and the position read have each had it since rounds 1 to 5.

New cases: a select that FAILED outside tmux and a move that FAILED, each
asserted to carry `tmux attach -t detsess:@32` and the "whatever it now holds"
wording; a `canon-lane` exiting 5 → exit 1 with the code named and nothing
launched; and a `canon-lane` unknown-subcommand 2 → the lane still binds through
the launcher, because an un-upgraded workstation is not a fault.

Measured on this tree (`origin/main` merged in at 2faa883):

    bash tests/test_lane_helpers.sh   2109 passed, 0 failed, 0 skipped, 0 pending

and the full `python3 -m pytest tests -q` under this workstation's lock was
**625 passed, 0 failed** in 1661 s on the tree twelve assertions ago. CI on
`03171c3`: `tests`, `tests-no-submodule` and `tests-windows` green,
`tests-macos` still running at the time of writing; all four were green on
`b8dddc1` and on `619c484`.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m

* Copilot round 12, three taken — an answer wider than the shell's integers WRAPPED into a lane nobody picked, `f` told an un-upgraded workstation to read something that does not exist there, and `lane ""` answered a different question

**`lane:1215` — A DIGIT STRING TOO LONG TO BE A POSITION IS NOT A POSITION.**
`pick=$((10#$answer))` WRAPS at the shell's integer width: `18446744073709551617`
comes back as `1`, so an answer far outside `1-N` reached the range check as a
number that IS in it and would have bound a lane nobody picked. That is the
wrong-lane outcome every fence in this file exists to stop, arriving through the
one door that reads a person's typing. The LENGTH is compared before the
conversion, against the digits of `$n` itself, because that is the widest answer
the question offered.

**`lane:873` — `2` IS THE CONTRACT'S OWN CODE IN THE `f` PATH TOO.** The listing
has told the two statuses apart since round 3 — an un-upgraded workstation named
as one and sent to `openRepoTools --install`, a read that FAILED named by its
code — and this path, the same read one answer later, collapsed both into "run
it by hand". A person who typed `f` on a workstation that has not taken the
install was told to read a subcommand that does not exist there.

**`lane:234` — AN ARGUMENT THAT WAS GIVEN AND IS EMPTY IS NOT "NO ARGUMENT".**
`lane ""` left `$name` empty, every `[ -n "$name" ]` below it read that as "no
lane was named", and a malformed named invocation silently became the LISTING —
and on a terminal the numbered pick, a question the person did not ask. It is
the contract's 64 at the door now, in the same terms the helper's own
whole-string fence uses.

AND THE FOURTH IS THIS PULL REQUEST'S OWN BODY: it said `--install` makes
EIGHTEEN artifacts while the code's invariant became NINETEEN when
`origin/main`'s #52 merged Amendment 12's `UserPromptSubmit` entry in beside the
`SessionStart` one. Corrected there, and derived from `INSTALLED` and
`HOOK_ENTRIES` rather than restated.

New cases: `f` answered on a workstation whose helper has no `next-free` —
refusing with 2, naming the install and starting nothing; the pick answered
`18446744073709551617`, refused as outside `1-N` with nothing bound (it is the
wrap that would have bound something); and `lane ""` and `lane --all ""`, each
the contract's 64, neither of them the listing.

Measured on this tree (`origin/main` merged in at 2faa883):

    bash tests/test_lane_helpers.sh   2120 passed, 0 failed, 0 skipped, 0 pending

CI is green on the head this sits on — `42d243a`, all four jobs, `tests-macos`
included — as it was on `03171c3` and on `619c484` before it.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m

* The suite's own wrapper is what failed on macOS, not a case in it: 2345 assertions take 45 minutes there and the bound was 40, so `TIMEOUT_SECONDS` goes 2400 → 3600 — measured, and with the real fix filed as #77

MEASURED, `b9018c1`, this branch with Amendment 17's suite section merged
beside Amendment 18 Addendum 1's — 2102 cases to 2345:

    tests-macos   03:18:03 → 04:03:37   1 failed, 654 passed in 2722.13s (0:45:22)
                  subprocess.TimeoutExpired: Command '['bash',
                  '…/tests/test_lane_helpers.sh']' timed out after 2400 seconds
    tests         03:17:59 → 03:34:46   green, the same 655 cases (16m 47s)
    tests-no-submodule  24m 53s green · tests-windows  26s green

**EVERY OTHER CASE IN THAT macOS JOB PASSED.** What the bound caught is the
suite being long on the slowest platform it is required to pass on — macOS at
roughly 2.7× the Linux time for the same file — and not a helper hanging, which
is the thing the bound exists for and still does at 3600. A `TimeoutExpired`
says nothing about which assertion was in flight, which is exactly the
unreadable failure the paragraph above that number already warns about: it is
why the same number went 900 → 2400 one amendment ago, for the same reason, and
the comment now carries both rounds.

ONE NUMBER AND NOT A PER-PLATFORM PAIR. A bound that is different where a
person is not looking is a bound nobody can reason about, and macOS is not the
only slow place: a legitimate run of this same tree took **2129 s on this
workstation** with six sibling suites building beside it, which is inside 2400
by four minutes and outside it on a busier evening. The job has no
`timeout-minutes` in `.github/workflows/tests.yml`, so nothing under it bites
before this does — GitHub's own default is 360 minutes, ten times this.

THE CAP IS NOT THE FIX. One bash file running real `git` several thousand times
serially for 45 minutes is the fix, and it is **opensoft/openRepoTools#77**,
filed and claimed by this lane with what to measure first — where the time
actually goes, before choosing between a per-section sandbox, parallelism the
wrapper's one-test shape would have to give up, and the cheap `git` settings in
the sandbox's own config. This commit moves the cap out of that work's way and
decides nothing about it.

No assertion is touched and no case is skipped.

Lane: openrepotools-3 (openRepoTools-3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 13, 2026
…olders verifies it, not on kill -0 (#56)

follow_transcript refuses to move a transcript out of another profile when a live process holds it (Amendment 18(h)). live_holders_of_id decided "live" with `kill -0` alone, one of the three tests the estate's liveness contract (record_is_live) makes, so an ended record or a recycled pid read as a live holder and refused a lane nothing held.

live_holders_of_id now asks `lanes-edit.sh transcript-holders <id>` (#52), the way dup_check already does: exit 8 is none, exit 2 is an old helper (a note, carry on), and any other nonzero sets FOLLOW_WHY and refuses with 2. It sets a LIVE_HOLDERS global and is called without a subshell so the refusal survives. The earlier tests added in the previous commit now pass.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants