Repository navigation
commit_push's true-conflict die stops claiming "nothing was pushed" when a later write from the same shared checkout may already carry the commit (#32) - #50
Conversation
…s pushed" when a later write from the same shared checkout may already carry the commit Checkpoint commit at the coordinator's request, ahead of merging main's #41 (Amendment 15, canon_lane, 8345d2f) into this branch. `bash -n lanes-edit.sh` and `bash -n tests/test_lane_helpers.sh` are both clean; the full `bash tests/test_lane_helpers.sh` run and `python3 -m pytest tests -q` are still in flight / pending re-run after the merge, and are not yet reflected here. Contents, inherited from the predecessor's uncommitted work and reviewed against issue #32's five occurrences (2026-09-13/14, Eagle, several lanes writing the register concurrently) and its own comments: - `commit_push`'s true-rebase-conflict branch (lanes-edit.sh ~1048-1062) reworded from an unconditional "rebase conflict on origin/main — nothing was pushed." to "…— not pushed by this attempt; a later write from this checkout may already carry it, so look before you retry.": three of the five observed occurrences named a commit that WAS already on origin by the time anyone read the die, carried there by the very next `lanes-edit.sh` write out of the same shared checkout. The RECOVERY lines are reordered to fetch-and-look at origin first, and the diff/reset instructions are now conditional on "only if it is not there". - Two new cases in tests/test_lane_helpers.sh: RACE-RETRY-WINS (a second clone lands a peer commit between this invocation's own pull and its push; asserts the final message names the attempt that actually pushed, never attempt 1's rejection, and that the row's text is written once, not twice) and OURS-CONFLICT (the peer rewrites the exact same row, a real conflict; asserts the abort truly leaves the worktree clean, the new wording appears, the old unconditional "nothing was pushed." does not, and the look-before-reset ordering holds). Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
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 1 day and 4 hours by commenting @sourcery-ai review. Upgrade to get a review now.
Reviewer's GuideUpdates Sequence diagram for shared-checkout conflict recoverysequenceDiagram
participant Lane as Lane write
participant Checkout as Shared checkout
participant Origin as origin/branch
participant Peer as Later lane write
Lane->>Checkout: git pull --rebase
Checkout-->>Lane: Rebase conflict
Lane->>Checkout: git rebase --abort
Checkout-->>Lane: Clean worktree
Lane->>Origin: git fetch origin branch
Origin-->>Lane: Recent origin commits
alt Commit already carried by later write
Lane->>Lane: Stop and inspect before retrying
else Commit not on origin
Lane->>Checkout: git diff origin/branch..HEAD
Lane->>Checkout: git reset --hard origin/branch
Lane->>Lane: Re-read and redo edit
end
Peer->>Checkout: Later write may carry dangling commit
Checkout->>Origin: Push combined commits
Flow diagram for true-conflict recovery decisionflowchart TD
A[Rebase conflict] --> B[git rebase --abort]
B --> C[git fetch origin branch and git log origin/branch -3]
C --> D{Commit already on origin?}
D -->|Yes| E[Do not assume nothing was pushed; inspect before retry]
D -->|No| F[git diff origin/branch..HEAD]
F --> G[git reset --hard origin/branch]
G --> H[Re-read LANES.md and redo edit]
E --> I[Exit 3 with attempt-scoped conflict message]
H --> I
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
🟡 Changes recommended
Conflict recovery still permits a duplicate redo after the commit is found on origin, and the exit-code documentation is inconsistent.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Updates commit_push conflict recovery to prevent duplicate writes when later shared-checkout activity carries a commit to origin.
Changes:
- Clarifies conflict messaging and recovery ordering.
- Adds race and same-row conflict regression tests.
File summaries
| File | Summary | Review findings |
|---|---|---|
tests/test_lane_helpers.sh |
Adds concurrent push and conflict coverage. | No findings. |
lanes-edit.sh |
Updates conflict messaging and recovery guidance. | Moderate: Reset and redo should be conditional on the commit being absent from origin. Nit: Update the exit-code table’s conflicting wording. |
Review details
Suppressed comments (1)
lanes-edit.sh:1180
- The gate is only attached to the diagnostic
diffline. If the preceding fetch/log shows that a later write already carried this commit to origin, the next two lines still instruct the caller to reset and redo the edit, which can create the duplicate this PR is meant to prevent. Make the reset and redo explicitly conditional on the commit being absent, and tell the caller to stop when it is present, as the existinglane-startrecovery does.
note " git -C $LANES_REPO diff origin/$LANES_BRANCH..HEAD -- ${CP_PATHS[*]} # only if it is not there: read back exactly what you wrote"
note " git -C $LANES_REPO reset --hard origin/$LANES_BRANCH # then drop the local commits (NOTE: also drops any"
note " # uncommitted peer edit in this checkout)"
note " then re-read LANES.md and redo the edit with lanes-edit.sh, on top of the peer's version."
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…g the die message they document, so a reader who already found the commit on origin is told to STOP instead of reset and redo it Copilot's round-1 review of #50 (5202640056) found two things this PR's own diff introduced without noticing: - lanes-edit.sh:188 (posted thread) — the exit-code table's row 3 still read "nothing was pushed, the edit is a local commit" after the die message a few hundred lines below it picked up the attempt-scoped wording ("not pushed by this attempt; a later write from this checkout may already carry it"). Reworded to match. docs/README-lanes.md:939 carries the same stale claim as the manual's own copy of the table and is reworded too. - lanes-edit.sh:1176-1181 (suppressed comment on :1180) — the RECOVERY block's "only if it is not there" qualifier was attached to the diagnostic `diff` line alone; the `reset --hard` and "redo the edit" lines after it stayed unconditional, so a caller whose fetch/log just showed the commit already on origin was still told, by those two lines, to reset and redo — the exact duplicate #32 exists to prevent. One new line between the look and the reset — "if that commit is already there, STOP — reset or redo now would write it a second time." — gates all three lines below it at once. Tests: - tests/test_lane_helpers.sh's OURS-CONFLICT case (~3600) asserts the STOP line's text and that it prints between the look and the reset it gates. - tests/test_repo_hygiene.py gains test_the_exit_3_documentation_agrees_with_the_die_message_it_describes, tying lanes-edit.sh's table and docs/README-lanes.md's copy to the die message's own wording so the three can't drift apart silently again. Verification: bash -n on both shell files, python3 -m py_compile on the test module, `bash tests/test_lane_helpers.sh` and `python3 -m pytest tests -q` both green (footers quoted in the PR). Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m
There was a problem hiding this comment.
🟡 Changes recommended
Recovery ancestry checking, manual guidance, and runtime-message coverage remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
tests/test_repo_hygiene.py:1910
- This test never reads the exit-3
dieat all; it only checks the table and manual. If the runtime message atlanes-edit.sh:1183regresses tonothing was pushedwhile those two docs remain unchanged, this test still passes, so it does not enforce the code/documentation contract its name claims. Extract the rebase-conflict die message and assert its attempt-scoped wording here.
assert "not pushed BY THIS ATTEMPT" in table, (
"lanes-edit.sh's exit-3 row no longer names the attempt-scoped wording")
assert "not pushed by this attempt" in manual, (
"docs/README-lanes.md's exit-3 row no longer names the attempt-scoped wording")
- Files reviewed: 4/4 changed files
- Comments generated: 2
- Review effort level: Lite
…l ancestry check, and the manual's prose paragraph and this test's own blind spot got the same fix as the table Copilot's round-2 review of #50 (5203033893) found three more things, all downstream of round 1's own fix: - lanes-edit.sh:1177 (posted thread) — `git log --oneline origin/$LANES_BRANCH -3` is not proof of absence: four or more later writes can bury this attempt's commit below a three-line window while it stays an ancestor of origin, and the next line would then wrongly permit reset/redo. Replaced with an actual ancestry test: `git -C $LANES_REPO rev-parse HEAD` is captured right after `rebase --abort` (HEAD is back to this attempt's own commit there) into `cp_sha`, and the look becomes `fetch ... && merge-base --is-ancestor $cp_sha origin/$LANES_BRANCH && echo ALREADY-THERE || echo not-there`. The STOP line now reads "if that printed ALREADY-THERE" rather than depending on what a human sees in a capped list. - docs/README-lanes.md:939 (posted thread, actually about the prose two lines above the table at :185-190) — the manual's own conflict-recovery paragraph still told readers to diff/reset/redo immediately, with no fetch, no ancestry check, no STOP. Reworded to mirror the ordered recovery lanes-edit.sh prints. - tests/test_repo_hygiene.py:1910 (suppressed comment, no thread) — round 1's new test read only the table and the manual, so a regression in the `die` MESSAGE ITSELF back to "nothing was pushed" left both docs unchanged and passed anyway. The die is extracted and checked first now, so all three have to agree with the thing that actually runs, not merely with each other. Tests: tests/test_lane_helpers.sh's OURS-CONFLICT case now asserts the ancestry command and its ALREADY-THERE/not-there framing, that a real commit sha is named (not a placeholder), and the STOP line's new wording; the ordering check (look before reset) now keys off the ancestry line. Verification: bash -n on both shell files, python3 -m py_compile on the test module, `bash tests/test_lane_helpers.sh` and `python3 -m pytest tests -q` both green (footers quoted in the PR). Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m
There was a problem hiding this comment.
🟡 Changes recommended
Fix failed fetch handling before reset/redo and complete the exit-3 documentation contract.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
docs/README-lanes.md:946
- This manual copy still describes exit 3 as only a rebase conflict, while the implementation also uses it for network timeouts and the exhausted dirty-peer path. The caller-facing table is therefore incomplete after this update; mirror the chosen complete exit-3 contract here.
| 3 | rebase conflict — not pushed by this attempt; a later write from this checkout may already carry it to origin, so look before you retry |
lanes-edit.sh:189
- Exit 3 is not only the rebase-conflict path:
git_timeout_dieat line 1002 and the blocked-peer exhaustion at line 1140 also calldie ... 3. Since this table says "one table, no two meanings", the new row remains incomplete and callers cannot interpret those refusals from the documented contract. Include these cases or assign distinct documented codes, and mirror the choice in the manual.
# 3 rebase conflict — not pushed BY THIS ATTEMPT; a later write from this
# checkout may already carry it to origin, so LOOK before you retry
- Files reviewed: 4/4 changed files
- Comments generated: 2
- Review effort level: Lite
… into "not-there", and the exit-3 table still named only one of its three causes Copilot's round-3 review of #50 (5203261904) found one real bug and one real gap, both in round 2's own work: - lanes-edit.sh:1181 (posted thread, real bug) — `fetch && merge-base ... && echo ALREADY-THERE || echo not-there` is left-associative: a FAILED fetch also falls to the trailing `||` and prints the SAME "not-there" a genuine non-ancestor does, silently waving the reset/redo path through against a ref that was never refreshed — recreating the exact duplicate this whole PR exists to prevent. Fixed by grouping the merge-base-and-echo pair in `{ ; }`, so the trailing `|| echo FETCH-FAILED` only fires when fetch itself failed (the group then never runs, against a ref nothing refreshed). Verified by hand against a real bare repo for all three outcomes: ALREADY-THERE (commit is an ancestor), not-there (commit is local-only, fetch succeeded), FETCH-FAILED (fetch against a bad remote). The STOP line now names FETCH-FAILED as its own STOP-and-retry outcome. docs/README-lanes.md:194 carried the same "answers non-zero → safe to redo" gap in its prose and is reworded the same way. - docs/README-lanes.md:946 and lanes-edit.sh:189 (suppressed comments, real completeness gap) — exit 3 is not only the rebase-conflict path: `git_timeout_die`'s network timeout and the six-attempts-exhausted path against a peer's uncommitted file (lanes-edit.sh:1140) both call `die ... 3` too, and the table's own claim is "one table, no two meanings on one number." Both rows now name all three causes; the attempt-scoped rebase-conflict wording (#32, round 1) is kept verbatim as the first of the three rather than replaced. Tests: - tests/test_lane_helpers.sh's OURS-CONFLICT case gains assertions for the FETCH-FAILED outcome (its own line, its own STOP-and-retry wording) and the updated ancestry-line text. - tests/test_repo_hygiene.py's exit-3 documentation test is whitespace- normalized (a phrase can legitimately wrap onto a comment-continuation line now that the row names three causes instead of one line) and gains a check that both the table and the manual name the other two causes ("timeout", "peer's"), not only the rebase-conflict one. Verification: bash -n on both shell files, python3 -m py_compile on the test module, a hand-built bare-repo check of all three ancestry outcomes, `bash tests/test_lane_helpers.sh` and `python3 -m pytest tests -q` both green (footers quoted in the PR). Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m
There was a problem hiding this comment.
🟡 Changes recommended
The publication check can still miss a rebased equivalent commit and permit duplicate entries.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
docs/README-lanes.md:196
- The manual repeats the exact-SHA ancestry check, but a later shared-checkout write that successfully rebases the dangling commit publishes a rewritten commit, not
<your commit>itself. This wording can therefore saynot-thereand authorize reset/redo after the change is already on origin; update it together with the implementation to check the published change rather than only the pre-rebase SHA.
eyeballing a capped log — `git -C <the workspace checkout> merge-base
--is-ancestor <your commit> origin/main` exits 0 once it is already there, in
which case STOP. Only once fetch succeeded AND this answers non-zero do you
tests/test_repo_hygiene.py:1952
- The new documentation test only checks for the substring
timeout, so it would pass the current push-only wording and would not catch the omitted pull-timeout path (git_timeout_dieis invoked forgit pull --rebaseat lanes-edit.sh:1166). Assert that the exit-3 documentation covers a timeout in both push and pull (or uses an explicitly generic network-timeout phrase) so this consistency test protects the full contract.
for name, flat in (("lanes-edit.sh's own exit-code table", table_flat),
("docs/README-lanes.md's own exit-codes table", manual_flat)):
for cause in ("timeout", "peer's"):
assert cause in flat, (
- Files reviewed: 4/4 changed files
- Comments generated: 3
- Review effort level: Lite
… rebase of the dangling commit changes its sha, so `git cherry` replaces `merge-base --is-ancestor` to compare by content instead Copilot's round-4 review of #50 (5203455553) found the deepest issue yet, squarely in the mechanism this whole PR is about: - lanes-edit.sh:1196 (posted thread, real bug in round 2's own fix) — `cp_sha` is captured right after `rebase --abort`, but the very peer write this recovery exists for (the branch just above this one: "the next write out of it pulls this dangling commit onto its own") carries it by REBASING onto a newer base, which gives the SAME change a NEW sha. `merge-base --is-ancestor $cp_sha origin/$LANES_BRANCH` checks the OLD sha, so it still answers "not-there" once the change is published under the new one — recreating the exact duplicate #32 is about, one layer deeper than round 3's fetch-failure bug. Replaced with `git cherry origin/$LANES_BRANCH $cp_sha | grep -c '^+'`, which compares by PATCH rather than sha and finds a rebase-carried commit either way. Verified by hand against a real bare repo: a peer rebases our dangling commit (sha changes, same patch) — `merge-base --is-ancestor` on the original sha answers "not-there" (the bug); `git cherry` on the same original sha answers 0, i.e. found (the fix). Re-verified for a genuinely local-only commit (1, not-there), a plain already-pushed commit with no rebase involved (0, empty `git cherry` output reads as zero `+` lines), and a fetch against a bad remote (its own FETCH-FAILED, never reaching a stale `git cherry`). Fetch and the content check are now two separate RECOVERY lines (matching `lane-start`'s own RV-B3 two-line shape) instead of one line chained with `&&`/`||`, since fetch's own success/failure no longer needs to gate a single compound exit code. - docs/README-lanes.md:196 (suppressed comment) and :949 / lanes-edit.sh:189 (open threads) — the same "SHA vs content" gap in the manual's prose, and a second, narrower completeness gap in both exit-3 rows: `git_timeout_die` fires for a timed-out PUSH (lanes-edit.sh:1112/1141/1160) or a timed-out PULL (lanes-edit.sh:1166), and round 3's own wording only ever named the push. Both rows and the prose are reworded: "a push or a pull", the table's opening no longer flatly claims "the commit stays local" (a hung push may already have landed), and the manual's recovery paragraph now describes the `git cherry` check instead of `merge-base --is-ancestor`. Tests: - tests/test_lane_helpers.sh's OURS-CONFLICT case is rewritten for the new two-line fetch/cherry recovery: the cherry invocation, that it names a real commit sha, the `grep -c '^+'` count, the fetch-then-check-then-reset ordering, and the simplified "if that printed 0, STOP" wording. - tests/test_repo_hygiene.py's exit-3 documentation test now requires both "push" and "pull" alongside "timeout" in each row, closing the gap a bare "timeout" check left open (round 3's own wording would have kept passing it forever). Verification: bash -n on both shell files, python3 -m py_compile on the test module, three hand-built bare-repo scenarios (rebased-carry, local-only, fetch-failure) for the cherry-based check, `bash tests/test_lane_helpers.sh` and `python3 -m pytest tests -q` both green (footers quoted in the PR). Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m
|
There was a problem hiding this comment.
🔵 Needs a closer look
Unresolved moderate comments remain for timeout recovery, cherry-range/path handling, and integration/documentation test coverage.
Review details
Suppressed comments (6)
Previously missed (1) — in code that hasn't changed since the last review.
lanes-edit.sh:1212
- These are copy/paste recovery commands, but the interpolated
$LANES_REPOis unquoted. The helper explicitly supports a checkout living anywhere (docs/README-lanes.md:205-208) and quotes it for its own Git calls, so a valid path such as/tmp/lane workspacemakes the fetch/cherry command fail and prevents the required lookup. Emit a shell-quoted-Cpath for every command in this recovery block (including the existing diff/reset lines).
docs/README-lanes.md:952
- By adding timed-out pushes and pulls to exit 3, this row also documents
git_timeout_die, but that shared runtime path still prints a push-specific recovery (a hung push often means it LANDED, thengit log ... -3andgit push; lanes-edit.sh:1006-1009). A timed-out pull reaches the same function at lanes-edit.sh:1168, so callers receive the wrong operation and the capped three-line lookup can miss a commit that a later shared-checkout write rebased and published. Make the timeout recovery distinguish pull from push and use the same successful-fetch/content check before permitting a retry.
| 3 | the edit is never a permanent loss, but this attempt cannot confirm whether it also reached origin — a rebase conflict (not pushed by this attempt; a later write from this checkout may already carry it to origin, so look by content before you retry), a network timeout on a push or a pull (a hung one often already landed), or six attempts exhausted because a peer's uncommitted file blocks every rebase |
docs/README-lanes.md:198
- The manual repeats the same unbounded
git cherrycheck: with multiple local commits, it counts unmatched patches from older commits as well as<your commit>. That can report nonzero even after the current edit was carried to origin, sending the reader into the duplicate-producing reset/redo path. Pass the current commit's parent asgit cherry's limit so this check evaluates only the edit being recovered.
actually published. `git -C <the workspace checkout> cherry origin/main <your
commit> | grep -c '^+'` compares by patch instead and still finds it; 0 means
lanes-edit.sh:1209
git cherry origin/$LANES_BRANCH $cp_shawalks every commit reachable from$cp_sha, not just the commit this invocation created. If an older local commit is still unmatched while the current commit's patch has already been carried to origin,grep -c '^+'remains nonzero, so the recovery permitsreset/redo and can recreate the duplicate this change is meant to prevent. Limit the comparison to$cp_sha(for example, pass${cp_sha}^asgit cherry's limit) and cover the multiple-local-commit case.
note " git -C $LANES_REPO cherry origin/$LANES_BRANCH $cp_sha | grep -c '^+' # BY CONTENT, not by sha: a peer's rebase changes the hash but git cherry still finds the same patch"
tests/test_lane_helpers.sh:3614
- The new assertions only inspect the recovery text and its ordering; they never execute the
fetch/git cherrylookup against the peer-rebased and local-only histories described in the surrounding comment. A regression such as using the wronggit cherryrange or treating a lookup error as0would still pass. Add an integration case that runs the lookup and verifies that a landed patch stops while a genuinely local patch permits recovery.
has "…checked by CONTENT (git cherry), never by a sha a peer's rebase can change" "$err" \
"cherry origin/main"
cp_sha_printed="$(printf '%s\n' "$err" | grep -oE 'cherry origin/main [0-9a-f]{7,40}' | head -n1 | awk '{print $3}')"
is "…naming a real local commit to check, not a placeholder" \
"$([ -n "$cp_sha_printed" ] && printf 'has-sha' || printf 'MISSING')" "has-sha"
has "…counting only the patches git cherry could not find upstream" "$err" \
"grep -c '^+'"
has "…the reset is now conditional on that look, not unconditional" "$err" \
"only if it is not there: read back exactly what you wrote"
tests/test_repo_hygiene.py:1926
- This new regression guard extracts only the rebase-conflict
dieand checks one shared phrase. Exit 3 is also produced bygit_timeout_dieand by the six-attempt peer-file branch, yet the later assertions merely look fortimeout,push,pull, andpeer'sin the docs; either runtime message can drift from the table/manual while this test still passes. Check all exit-3-producing messages (or derive the documented causes from every call site) so the test actually enforces the agreement claimed by its name and docstring.
m = re.search(r'die "(rebase conflict on origin/\$LANES_BRANCH[^"]*)" 3\b', src)
assert m, "no exit-3 die message found in lanes-edit.sh to check the docs against"
die_text = m.group(1)
assert "not pushed by this attempt" in die_text, (
"the exit-3 die message itself no longer names the attempt-scoped "
f"wording the table and the manual are checked against: {die_text!r}")
- Files reviewed: 4/4 changed files
- Comments generated: 0 new
- Review effort level: Lite
…t 17(a) renamed that act and the case came from a branch written before the rename Both Linux CI jobs went red at `5c162df` — the merge of `main` at `b5244e2` — on one assertion of `test_a_skill_whose_bytes_are_right_and_whose_mode_is_not_is_re_moded`, and the defect is a name spelled twice in one file. `skill_paths()` DERIVES its two paths from `SKILL_NAMES`, so on this branch it hands back `handoff`'s — Amendment 17(a) renamed the act from `lane-swap` to `handoff` and kept `lane-swap` as an alias file — while the case that #40 and #44 added on `main` asserts the printed line with the literal `lane-swap:`. The installer was right: it chmod'ed the path it was given and named the skill that owns it (`handoff: … (mode restored to 644)`). The merge that brought a case written before a rename on to the branch that made it is exactly where a spelled name goes stale, and the file's own header says so five lines above the constant it should have used. Both assertions take `{SKILL_DIR_NAME}` now, which is the same derivation the paths take. Merges `origin/main` at `a954435` (#50, `commit_push`'s conflict die and its `tests/test_lane_helpers.sh` § #32) on top of `b5244e2` (#44, the installer's mode stamping and unwritable-target refusal; #53's `skills/restart/SKILL.md`). Co-Authored-By: Claude Fable 5.1 <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
…207-line suite section, and 66b4fb9 = #53's `/restart` skill fence) — NO CONFLICT in the two files both sides edit, and #53 has already taken two of the six sites #58 was filed for `origin/main` moved twice more while this branch was being reviewed, and both moves land in files it also rewrites: lanes-edit.sh 51 ++- #50's `commit_push` true-conflict die, beside this branch's `lane_groups`, `lane_next_free` and `canon-lane` arm tests/test_lane_helpers.sh 207 +++++ #50's own `== #32 ==` section, beside this branch's Addendum 1 section skills/restart/SKILL.md 19 ++- #53's `lread` stderr fence, beside this branch's line saying the CLI is retired tests/test_repo_hygiene.py 91 ++- #53 and #50 tests/test_openrepotools_command.py 6 +- #53 docs/README-lanes.md 17 ++- #50 `git merge` resolved every hunk itself — `Automatic merge went well` — and the two sides never met on a line. The merged `skills/restart/SKILL.md` carries BOTH: #53's `lread`'s `grep -v -e 'not fetching; reading origin/'` fence, and this branch's sentence that `restart <lane>` was the word "until Amendment 18 Addendum 2 retired it from the PATH". The skill itself is unchanged by this branch, which is Addendum 2's own division: `/restart` is the INSIDE half. AND #53 TOOK TWO OF THE SIX SITES #58 was filed for — `tests/test_repo_hygiene.py`'s module docstring and `tests/test_openrepotools_command.py`'s "There are FOUR files to answer for" — which is the same act on the same drift, from the other direction. The issue is updated with what is left; nothing of it is this branch's to fold in, because every remaining sentence is false on `main` without this branch's diff. Measured on the merged tree, in this worktree: bash tests/test_lane_helpers.sh 1915 passed, 0 failed, 0 skipped, 0 pending (+31 over this branch before the merge, which is #50'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 280 passed The full `python3 -m pytest tests -q` under this workstation's lock is in flight, 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
…ndoff` on PATH, `/handoff` with `/swap`, `/lane-swap` and `/ctx`, the record naming the agent and its transcript, and `lane-start --agent` resuming a lane from any agent the table carries (#47) * WIP (swap-imminent commit, pushed unverified): a handoff is the swap under one name — `lane-handoff` on PATH, `/handoff` with `/swap` and `/lane-swap` as aliases, `/ctx` = `/handoff --restart`, the record naming the agent and its transcript, and `lane-start --agent` resuming the lane from any agent the launcher table carries WIP, AND THE WORD IS IN THE SUBJECT BECAUSE THE PROOF IS NOT IN YET. Every file parses (`bash -n` on the four commands and the suite, `py_compile` on the three pytest files) and the bash suite was green through every pre-existing section on the run before this one; the Amendment 17 section this commit adds had NOT yet been reached when the swap was called, and `python3 -m pytest tests -q` has not been run at all. Nothing here is merged on that basis: the next session re-runs both suites and takes what they say. lane-collision-protocol **Amendment 17** (ratified 2026-09-14T09:45:33Z, revision 2 with clause (f)), **Amendment 18(d)** and **Addendum 2**; #36, acts 1-8. THE ACT HAS ONE NAME AND THREE DOORS. `lane-handoff` on PATH (new file, `INSTALLABLES` 11 -> 12) is the `/lane-swap` skill's steps as one command: the identity triple fixed through `lanes-edit.sh`, the handoff file refreshed with a fresh Rule 3 top block, every running writer polled — `git status --short` and `git log @{u}..` of every worktree under the lane's checkout `.claude/worktrees/` and `<projects>/.lane-worktrees/<lane>/` — `PAUSED` written with Amendment 11(c)'s payload plus Amendment 17(b)'s `agent <name>; transcript <id|none>`, and the one restart line printed. The top block's template gains a WRITERS section (worktree, branch, brief, last commit, dirty and unpushed counts) and the first line *"relaunch every writer below from where it stands"*; it is PREPENDED and the file is never rewritten, so what was there stays below a `---` rule as history. `skills/handoff/SKILL.md` carries the steps with their history (a rename, so `git log --follow` reads straight through); `skills/lane-swap/SKILL.md` and `commands/swap.md` name it and add nothing, and `commands/handoff.md` and `commands/ctx.md` are new. `--exit` (Amendment 18(d)) types `/exit` into the lane's own pane — the pane from the LIVE record's own `tmux` field — because a handoff to another place is a handoff and not a restart. `--restart` is `/ctx` (clause (f)): `tmux respawn-pane -k` on that same pane through the launcher, with `LANE_START_FRESH=1 pclaude --lane <lane> <profile>` — never `restart <lane>`, which Addendum 2 takes off the person's PATH. BOTH REFUSE BEFORE THEY TOUCH THE PANE where the record could not be written: *"a pane is never respawned over an unrecorded lane"*. `lanes-edit.sh`, ADDITIVELY, because #41 is landing in the same file: `pause_subfields_check` validates the two sub-fields in the WRITER (`write_event`, from any caller) and reads them with `all` so that `agent not a key` is refused rather than read as `not`; `swapped` gains a sixth and a seventh field, EMPTY for every record written before the amendment; `window_lane`'s reader names that row's fields in full so a short list cannot swallow them; four new reads (`lane-agent`, `lane-transcript`, `lane-last`, `workspace-root`); and `log … --utc <UTC>` dates the late record by its own field. `lane-start --agent <name>` (clause (c)): the default is the agent of the last `PAUSED`, else `claude`. `claude` with its transcript here resumes by id exactly as Amendment 6 has always done; otherwise a launcher table — `claude` takes a named new session with the handoff's TOP BLOCK as its first prompt, `codex` takes the top block positionally, and a name the table does not carry is a refusal naming the two it knows. The Rule 3 stamp and the row's session cell are written BEFORE the launch, the stamp naming the agent (`RESUMED by codex <id>`) and the cell in that agent's own spelling (`Codex <id>`). ACT 6, the transcript follows the lane, shrunk to what the measurement of 2026-09-14T12:35Z left of it: `lane-start` SAYS which projects directory it found the transcript in and whether that directory is SHARED (`readlink -f` equal across profiles), and MOVES the `<uuid>.jsonl` and its sibling `<uuid>/` only where another profile's `projects/` really is another directory, leaving the pointer `<uuid>.jsonl.moved-to-<profile>-<UTC>` where they were. A uuid whose holder is LIVE in another profile is a refusal naming the pid, its window or `bg`, the profile and the retire act (Amendment 18(h)) — never a move, because a copy leaves two live transcripts of one uuid to diverge. ACT 7, `--late`, AND THE RULE IT IS BUILT ON. A lane's state is its log's last lane-kind line IN FILE ORDER and the log is append-only, so a late `PAUSED` appended after the relaunch's `RESUMED` would make a lane that is RUNNING read as paused. The simplest rule that keeps every `binding` and state read correct is to refuse to write a line that can no longer be last: `--late` REQUIRES `--at <UTC>` — a late line is dated by its own field — and is written only where the lane's last lane-kind line is a `STARTED` or `RESUMED` older than that instant. Every other case is a refusal that writes nothing, made BEFORE the writers are polled and before the handoff is refreshed, so a refused `--late` leaves the lane exactly as it found it. Stated in the code, in the skill and in the manual. Docs: `docs/README-lanes.md` gains "The handoff (Amendment 17)" — the act under its three names, the two sub-fields and the reads that answer for them, the launcher table, `/ctx`, `--late` with its rule, and the transcript rule; `README.md`'s install section moves to twelve files, three skills, three command files and twenty-five artifacts, with the cap raised in a dated entry. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * WIP (second swap-imminent commit, unverified): the anchor `lane-start` appends a non-Claude agent's id to is the PUBLISHED row's last id, and four Amendment 17 cases in the suite ask what they say they ask WIP FOR THE SAME REASON THE FIRST ONE IS: this is the predecessor's working tree at the moment the swap was called, committed so that nothing is lost and so that the next run's diff is against a tree that parses. The Amendment 17 section had not been through a green run when these lines were written; the suites decide what survives. `lane-start`: the id `append-session-id` is anchored on for an agent that is not Claude is `$row_sid`, the LAST id of the published session cell, which is the same anchor the minted-uuid append uses and for the same reason — `append-session-id` refuses unless its anchor occurs exactly once in that cell, so a stale copy of the row is a refusal that prints the act rather than a corruption. `$cell_anchor` is not that id: it is set only where a session is live in this window whose uuid the cell lacks, which is never the case for the launch of an agent that is not Claude, so the branch as written took the empty-anchor door every time. The suite, four cases that were asking something other than what they read: `lane-handoff`'s stdout is kept in `hf1_out` because a `swapped` read stands between the run and the assertions that quote it; the unwritable-record `/ctx` case is asked from a window NO live record names, so that there is no transcript uuid anywhere and `write_event` really does refuse (Amendment 11(e)) rather than being handed one by the window read; the shared-`projects/` case gets its own lane `repoSH-1`, because `repoFR-1`'s cell has since gained the uuid the `LANE_START_FRESH=1` run minted and that one has no transcript at all — the other branch; and the live-holder record is named for ANOTHER lane, which is the shape Amendment 18(h) is written about: a second process on one transcript that the lane's own liveness read does not see. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * `lane-handoff` is a command in the index and not only after `--install` chmods it: the executable bit, the macOS parse step and the mode of every placed file are each derived from a list the next file joins by existing THE PROOF THE TWO WIP COMMITS SAID WAS NOT IN YET. `bash tests/test_lane_helpers.sh` is **1630 passed, 0 failed, 0 skipped, 0 pending**, with the 129 cases of `== Amendment 17: the handoff is the swap ==` among them — the section that had not been reached when the swap was called. Nothing in the amendment's own code needed changing for that; what this commit carries is what reading the file alongside the suite found. `lane-handoff` WAS COMMITTED 100644. Every other file `--install` places is 100755 in the index and `repos.tsv`, which is data, is not. The installed copy runs anyway — `--install` stamps 755 on everything it places — so the defect has no symptom there; what it costs is `./lane-handoff` from a clone, and the `cp -p` this suite copies it with, which takes the mode git recorded. The suite's own case did not see it because the fixture ran `chmod 755` on the copy. That line is gone: the case now reads back the mode THE REPOSITORY carries, which is the only mode anyone else gets. AND THE THREE LISTS IT WAS MISSING FROM, each closed so that the next file joins by existing rather than by being remembered: * `ALL_BASH` gains it through a new `HELPER_BASH`, with the reason it is in neither list beside it stated where the other two state theirs — not `SHIPPED_BASH`, because `R-A11-11` (*a swap is never left unwritten*) is why it does not `set -e`; not `LANE_BASH`, because it carries no copy of Amendment 9(a)'s workspace resolver, resolving no workspace and reading the one path it needs back from `lanes-edit.sh workspace-root`. So 837 lines of shipped bash now take the parse gate, the LF index, the flag-spelling rule and the four bash-3.2 text rules; it passes all of them as written. * the macOS job's `/bin/bash -n` step, which is a HAND-WRITTEN LIST and the only place the README's bash-3.2 claim is asked of bash 3.2. `lane-handoff` was not in it and nothing said so: the job goes green and proves one file less than it claims. `test_the_macos_job_parses_every_bash_file_this_repository_ships` reads that step and holds it against `ALL_BASH`, per name, because `bash -n a b` parses `a` and hands `b` to it as `$1`. * the index mode, read out of the installer's OWN `INSTALLABLES=( … )` rather than a list in the suite — 755 for a command, 644 for the `.tsv` that is read and never run. That is the test that would have caught this the moment `lane-handoff` joined the installer's list, with nobody coming here first. THE COUNTS THAT STATE THEMSELVES, finished: `AGENTS.md` and `docs/README-lanes.md` still said eleven files, two skills and eighteen artifacts — twelve, three, three and twenty-five — and four docstrings in the installer's tests counted the same way. `README.md`, the installer's prose and every derived count moved with the first WIP commit; these are the ones that state rather than derive. lane-collision-protocol **Amendment 17** (in force 2026-09-14T09:45:33Z), **Amendment 18(d)** and **Addendum 2**; #36. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * The `/ctx` respawn types the word Amendment 18 Addendum 2 gives it — `lane <lane>` where that word is on PATH, the launcher where it is not, and never a `lane` this estate did not place Addendum 2 clause (i-8), in force 2026-09-14T16:50:32Z, says the respawn after `/ctx` *"relaunches the lane's own pane with `lane <name>` (its parked branch is exactly Amendment 11(i)'s act), or through the launcher directly"*, and its adoption line names this issue: *"#36 (`/ctx`) respawns with `lane <name>`"*. What this branch had was the second half of that sentence and never the first: every respawn was `pclaude --lane <lane> <profile>`. So `lane-handoff --restart` now asks PATH for the word and types it — `LANE_START_FRESH=1 lane <lane>`, with NO profile argument, because `lane <name>`'s parked branch reads the record this command has just written for the lane's directory and profile and asks nothing. The launcher line stays as the door the same clause leaves open, for the workstation where `lane` is not installed yet: it arrives with #43 and this act ships first, and a respawn is the one act no later refusal can undo — the pane's process is already dead when the new command fails — so the word is used only where this command can SEE it. AND NOT EVERY `lane` ON A PATH. `pclaude` is nobody else's name; `lane` is an ordinary English word, and a pane respawned over somebody else's `lane` is a pane the person cannot get back. The file is read for the string every bash file this toolset ships carries (`lane-collision-protocol`, in all six of them), and one that carries none is passed over for the launcher with a line saying which file was passed over — the safe side of the two, because it still starts the lane. The skill and the manual say the same thing in the same order, and the skill's snippet is now the choice the command makes in code rather than one branch of it. TESTS, in `tests/test_lane_helpers.sh` § "Amendment 17", all three on a PATH THIS FILE BUILDS: `a17_path_without_lane` drops every directory holding a `lane` from the section's PATH, so the cases ask their own question on a workstation that has the word (#43 places it in `~/.local/bin`) and on one that does not. `repoHF-8`, with a fake `lane` carrying the marker, asserts the respawn line is that file and `repoHF-8`, with the FRESH seam still on it, with no profile argument, and never the launcher; `repoHF-9`, with a fake `lane` carrying none, asserts the launcher line and the sentence naming what was passed over; `repoHF-5`, on a PATH with no `lane` at all, keeps every assertion it had. VERIFIED HERE: `bash -n lane-handoff`, `bash -n tests/test_lane_helpers.sh`. Suite before this commit, at e25b54c: 1630 passed, 0 failed, 0 skipped. THE RUN OF THE SUITE WITH THESE CASES IN IT FOLLOWS THIS COMMIT — it is pushed now because a context clear of the driving session is imminent and an unpushed change is a change the next session cannot see. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * The `/ctx` respawn reads the `lane` it found through a redirect, because the job that runs this suite under BSD `head` is the macOS one and a portability question is not one to answer inside the act that kills a pane `lane-handoff --restart` reads the head of the `lane` it found on `PATH` for the string every bash file this toolset ships carries, and read it as `head -c 8192 -- "$lane_word_path"`. The macOS job runs `python3 -m pytest tests -q` — which is this shell suite, through `tests/test_lane_helpers_suite.py` — under BSD `head`, whose option handling is its own; `head -c 8192 < "$file"` hands the file to the utility as a file descriptor and asks nothing of its argument parsing. Same bytes, same guard, one fewer question. Parsed: `bash -n lane-handoff`. The suite at f8a4ce6, with the three PATH cases this guard exists for, was 1821 passed, 0 failed, 0 skipped; the run over this line follows. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * A handoff names the KIND of clear it was written for, because an in-process clear does not kill this lane's writers — the top block runs `ListAgents` first and relaunches only what it does not name; and the two stale tests CI found in the same breath THE MEASURED FACT, in this lane on 2026-09-14: a harness `/clear` mints a NEW TRANSCRIPT ID IN THE SAME PROCESS. Every subagent the lane has running survives it — only the tool calls they had in flight die, a `Bash` killed that way exiting 137 — and the handoff written before that clear said *"every agent dies with this context; relaunch every writer"*, which was FALSE. A next session that believed it would have put a SECOND writer on a worktree the first one still held, and two writers on one worktree is how the work in it is lost. Only a NEW PROCESS takes the writers with it: the pane respawned (`--restart`, which is what `/ctx` does here), the session ended (`--exit`), or the launcher run after a plain handoff. So the act now records WHICH IT WAS and the top block tells the next session what to do about it: * `lane-handoff --in-process` — the clear that keeps this process. Refused with `--restart` and `--exit`, because each of those replaces the process every writer is a child of. Default, and every other form: `respawn`. * the `PAUSED` payload carries `kind <in-process|respawn>` after `agent` and `transcript`, on the same argument those two were added on: a reader written before the field stops at the sub-field it knows. It is in the RECORD and not only in the handoff because the record is what a later reader and a launcher have. * the top block's act (3) is now, in both kinds: **RUN `ListAgents` FIRST. A WRITER STILL LISTED IS ALIVE AND OWNS ITS WORKTREE: DO NOT RELAUNCH IT. Relaunch only writers `ListAgents` does NOT name, and only after `git status --short` and `git log @{u}..` in that worktree say where it stood.** The `WRITERS` section repeats the rule where the list is, and the kind adds its own sentence — *expect `ListAgents` to name none of them* for a respawn, *expect it to name every one, and relaunch NONE* for an in-process clear. The skill, `commands/ctx.md` and the manual say the same in the same order, and the skill carries the table of the two kinds so that a session writing the block by hand writes the same two texts. AND THE TWO TESTS CI FOUND, both of which were green here for the wrong reason: * `test_the_swap_skill_snippet_and_its_prose_spell_the_same_command` read `skills/lane-swap/SKILL.md`, which Amendment 17(a) turned into an ALIAS that restates no step — so it was asserting over an emptiness. It now reads the installer's own `SKILL_NAME`, which is where the canonical skill is declared once, and every assertion it makes lands on the file that carries the act (2 `restart_cmd=` branches, no `pclaude run` unexplained, `--lane` leading). * `test_no_committed_file_names_a_host_absolute_path` caught three `/home/b/` spellings in the manual's examples. They are `~/projects/repoHF` and `~/.claude/projects` now, which is what every other example in that file uses. AND ONE SUITE AT A TIME, in `AGENTS.md` where an agent is sent to read it: six lanes build in sibling worktrees of this checkout and FOUR SUITES RAN AT ONCE under the guard they had been given — `pgrep -af 'python3 -m pytest' | grep -v pgrep >/dev/null` never waits where `grep` is the harness's own shell function (status 1 when its stdout is `/dev/null`), and its unanchored pattern matches the guard's own command line. The working form is an anchored `pgrep -fc` with its pattern split so it cannot match itself, then `flock` so the waiters cannot all start together. The cap entry for those nineteen lines is in the test that holds it. SUITE: `bash tests/test_lane_helpers.sh` — 1821 passed, 0 failed, 0 skipped at `f8a4ce6` (the merge of Amendment 15); **1844 passed, 0 failed, 0 skipped** with the `kind` cases of this commit (+23: the record's kind, the block's ListAgents rule and its two texts, the `--in-process` record and block, the pane untouched, and both refusals). `bash -n` on `lane-handoff` and the suite. The pytest run that would have covered the two test files above is NOT started: this workstation has three suites running under other lanes and this lane is shutting down on Brett Heap's word; CI on #47 is the authority and has already run these two tests red on their pre-fix bytes. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * A pane is never respawned over a record that is only part written, a transcript never half-moves between profiles, and the launch line is quoted by something bash 3.2 and BSD `sed` both read — Copilot rounds 1 and 2, Amendment 17 Addendum 1, and the suite's own lock THE macOS JOB'S THREE RED CASES WERE THIS BRANCH'S OWN, and the defect is one line that has been in `lane-start` since before this amendment: quoted() { printf '%q ' "$@" | sed 's/ $//'; } Two defects in it, and only their combination is fatal. `printf '%q '` renders differently under BASH 3.2 — what Apple ships, and what `tests-macos` runs (`/bin/bash` 3.2.57 on macOS 26.6.2): bash 4.4 and later give a multi-line argument back as `$'…\n…'`, one readable and runnable word, while 3.2 has no such form and escapes every space and every byte of every multi-byte character with a backslash of its own. And the PIPE made that fatal rather than ugly: under `set -o pipefail` the function's status is the pipeline's, and BSD `sed` in a UTF-8 locale (`LC_ALL=en_US.UTF-8` there) refuses bytes that are not a valid sequence — `RE error: illegal byte sequence` — which is exactly what 3.2's `%q` makes of the em dash every document in this estate is written with. `quoted` is a BARE command in the `--no-launch` branch, so `set -e` ended `lane-start` there: exit 1, empty stdout, three assertions red and no other platform able to see it. The one caller that passes a multi-line argument is this amendment's own — the `--no-launch` print of a launch carrying the handoff's top block as its first prompt — which is why it went red here and nowhere before. It is now POSIX quoting of the file's own, in `lane-start` and `lane-handoff`: safe words bare, anything else in single quotes with `'\''` for its own. `restart` carries the same line and is untouched — #43 deletes that file in flight and a conflict there costs it a round. COPILOT ROUND 1 (`db206db`), the four inline findings: * `lane-handoff:842` — the `--restart`/`--exit` gate tested only the object-log write. The record is THREE writes: the ROW is what every other lane reads this lane's state from, and the HANDOFF's top block is literally the respawn's first prompt. It now gates on all three and each refusal names the act that clears it. * `lane-start:1692` — `readlink -f` is not stock macOS userland, so `real_of` fell back to the UNRESOLVED path, two spellings of one directory compared as two directories, and the shared-`projects/` case — every case on a launcher-configured workstation — would have moved a transcript that never needed moving. Now `( CDPATH=''; cd -P -- "$1" && pwd -P )`. * `lane-start:1781` and `:1788` — the sibling `<uuid>/` move's status was discarded and the pointer's write was suppressed. The `.jsonl`, its sibling and the pointer are ONE TRANSACTION now: each step is checked, a step that cannot complete puts back what the steps before it moved (`ft_rollback`) and refuses, and the refusal names anything the rollback itself could not undo. * DECLINED, both mode findings (`lane-handoff:1`, `tests/test_repo_hygiene.py:198`): `git ls-tree db206db lane-handoff` → `100755`, and `git ls-files -s` says the same today. What they read is that test's docstring describing the FIRST Amendment 17(a) commit; it now names `e25b54c` as the commit that fixed the mode and points at `git ls-files -s`. The same stale rationale in the A17 fixture is corrected. From round 1's SUPPRESSED list: `lane-start:1776` (`--dry-run` moved the transcript before reaching the exit that says it moved nothing), `lane-handoff:612` (`handoff_written=1` set whether or not the write succeeded), `lane-handoff:494` (a failed `mktemp` gave a WRITERS section with a count and no entries), `lane-handoff:395` (the record's `transcript` came from `$sess_uuid`, not the resolved `$uuid`, so a handoff from a bare shell wrote `transcript none` about a conversation whose id the same line carries), `lane-start:1687` and `:1719` (`$CLAUDE_PROFILES_HOME` ignored by both profile walks, though this repository's own README documents it), `lane-start:1881` (a failed `lane-agent` read launched the default agent, which is the one thing that read's own refusal forbids — it now refuses and names `--agent`), and `lane-start:1997` (a lane with NO ROW wrote `RESUMED` as its object log's first line). COPILOT ROUND 2 (`ac0c697`): * `lane-handoff:303` — the read fence treated exit 2 as "no answer", but `canon_lane` spends 2 on a register holding two rows that differ only by case. `lane-handoff` fell through to the next rung and would have renamed a window, written a record and respawned a pane for whichever lane it answered with. The refusal is now asked for by its own words, as `lane-start:755-767`, `lane-end` and `restart` ask for it, and relayed. * `lane-handoff:360` and `:403` — the lane is canonicalised through `canon-lane` BEFORE the window rename, so the row's spelling is what the rename, the top block, the payload, the log's lane field and the restart line all carry; and a `--lane` that is not a lane name is refused there rather than after the window has been renamed. * `lanes-edit.sh:3946`/`:3963` — `pause_subfields_check` accepted a payload carrying exactly one of `agent`/`transcript`, and an empty value parsed like an absent one. Both are refused now, with the value judged before the pair so a malformed one is still refused for the malformation. * `lanes-edit.sh:5723` — `--utc` was taken for every verb and only its shape was checked; it is the late `PAUSED`'s seam and is refused for everything else, because file order is what every state read means by "last". * `lane-start:1899` — `lane-transcript` answers for the LANE, so `--agent codex` over a record that says `agent claude` would have appended `Codex <claude uuid>` to the row's cell. The id is carried only where the recorded agent IS the agent being launched. * `lane-start:1982` — the launched agent's name is exported, so a `codex` session's own handoff does not record `agent claude`. FILED, NOT FOLDED IN: #55 (`swapped`'s first field is the LOG's spelling and not the register row's — Amendment 15's claim, landed on `main` with #41, so it predates this diff) and #56 (the cross-profile transcript-holder scan tests only `kill -0`, where `lanes-edit.sh`'s one liveness contract tests three things). Both claimed by this lane. AMENDMENT 17 ADDENDUM 1, in force 2026-09-14T20:59:31Z (brettheap/new-workstation#40, `26a0d5b`), whose adoption line names this issue: * (h) the record names the KIND, and there are THREE: `--in-process` writes `in-process`, `--restart`/`--exit`/`--late` write `respawn`, and a PLAIN handoff writes `unknown` — the act that follows it is not this command's to know. The word is in the payload (`; kind <k>`) and in the line's FREE TEXT after the why (`clear in-process`, `clear respawn`, `kind unknown`), which is the addendum's own spelling. * (i) the WRITERS section is a list to COUNT, not a list to relaunch, and its first line is the clause's own words under EVERY kind. * (j) the kind says what to EXPECT and never what to do, so `relaunch every writer below` and `relaunch NONE of them` are both gone from the template. * (k) one worktree, one writer, in the first acts, with the retire act for a second one found. AND THE SUITE'S OWN LOCK (#51). Four `python3 -m pytest tests -q` runs were live at once on this workstation on 2026-09-14, every one past a guard copied into four briefs: `pgrep -af '…' | grep -v pgrep >/dev/null` never waits, because `grep` under the harness is a shell FUNCTION whose status is 1 when its stdout is `/dev/null`, and its pattern is unanchored besides. `tests/run.sh` is that guard written ONCE — wait, take `${TMPDIR:-/tmp}/openrepotools-pytest.lock` (`flock`, or a `mkdir` lock on macOS, which has none), wait again inside it, then run the suite — named in `AGENTS.md` and `docs/README-lanes.md` as the way to run it, parsed by the macOS job with every other bash file here, and pinned by a hygiene test that holds the lock path in both places. Closes #51 (with #36). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * A lock nothing can validate is stale and not eternal, a transcript never moves onto an occupied name, and the two acts that reach a handoff through a symlink resolve it the way macOS can — Copilot round 3 The round before this one went green on all four CI jobs, macOS included, which is what made these findings readable rather than hidden behind three red cases. `tests/run.sh` — THE LOCK THIS PULL REQUEST SHIPS AS THE WAY TO RUN THE SUITE, and three things a `mkdir` lock has to get right that the first shape of it got wrong (`tests/run.sh:109`, `:113`, `:120`): * A LOCK WITH NO PID IN IT IS STALE, NOT ETERNAL. The pid is written the instant after `mkdir` returns, and a run killed in that instant left a directory nothing could validate — which the loop waited on for ever, blocking the estate's one test entry point for everybody. * A TAKEOVER IS A CLAIM, NOT A DELETE. Two waiters that both saw a dead holder both `rm -rf`'d the name, and if one had already re-made it the other deleted the NEW holder's lock and both suites ran. It is a `mv` now — one rename, one winner, the loser loops — with the claimed directory RE-READ after the rename, because the only way a live holder can be behind that name is if it appeared between the read and the rename, and then it is put straight back. A stale state must also hold for three polls before it is claimed at all. * THE SIGNAL HANDLERS EXIT. `trap 'rm -rf …' EXIT INT TERM` released the lock on a Ctrl-C and then carried on running the suite, which is the collision the lock exists for. `lane-start:1878` — AN OCCUPIED DESTINATION IS REFUSED BEFORE ANYTHING MOVES. `mv <dir> <existing dir>` does not fail: it moves the source INSIDE it. A `<uuid>/` already in this profile would therefore have taken the other profile's sidecar as `<uuid>/<uuid>/`, been marked moved, and left the lane resuming with the transcript's state split from the transcript. Which of two `<uuid>` directories is this lane's is not a thing to guess at, so the read names what is there and stops — and a name that is NOT a directory still falls to the transaction below, where `mv` really does fail and the rollback puts the transcript back. `lane-handoff:812` and its twin in `lane-start` — `readlink -f` IS NOT IN THE STOCK macOS USERLAND, which is the same rule `real_of` took last round. Where it is absent the fallback hands the path back UNRESOLVED, so `hs_repo` — which git answers with a PHYSICAL path — and `hs_rel`, derived from the symlinked spelling, disagree; `git add` then refuses the pathspec and the handoff this act has just written, or the Rule 3 stamp it has just spliced, is left uncommitted. A handoff is reached through the estate's `handoffs` symlink on every workstation that has one. Both now resolve the PARENT with `cd -P`/`pwd -P`. `lane-handoff:1052` — `head -c 8192 … | grep -q` TAKES SIGPIPE UNDER `pipefail`. `grep -q` exits the instant it matches, `head` writing the rest of a word larger than 8192 bytes dies with 141, and the pipeline's status is that — so the estate's OWN `lane` was classified as somebody else's and `/ctx` respawned through the launcher instead of the word. `grep` reads all of the bounded input now, and the suite's fake `lane` is padded past 8192 bytes so the three PATH cases prove it. `lane-start:2132` — A LAUNCHER THIS WORKSTATION DOES NOT HAVE IS REFUSED BEFORE ANY WRITE. The row, the object log and the Rule 3 stamp are all written before the `exec`, so a `codex` that is not installed recorded the lane as RESUMED and then exited 127: a false transition in two append-only files that a person has to repair by hand. `lane-handoff:860` — the `window` sub-field's fence gains `; ` and `"`, the two its own comment already claimed it dropped. `window` is two space-separated refs in ONE sub-field, so a `; ` inside it is read back as the end of the sub-field and every reader recovers a window ref that is not the lane's. FILED, NOT FOLDED IN: #65 — the `/ctx` respawn recognises this estate's `lane` by a string in the word's own head, which is a heuristic and not provenance: any executable can carry it. Tightening that to the directory `--install` places these commands in is a change to the behaviour Amendment 18 Addendum 2 (i-8) adopted, and to three of this suite's PATH fixtures with it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The codex-absent case asks its question on a PATH this file built, because the workstation it was written on HAS codex installed Four assertions of the round-3 case `--agent codex on a workstation with no codex is refused` went red on Eagle and green on every CI runner, which is the shape of a fixture asking a different question rather than of a defect: the workstation carries `~/.npm-global/bin/codex`, so the refusal correctly did not fire and the launch went ahead. `a17_path_without <word>` is the generalisation of the `a17_path_without_lane` the three `lane` cases already use, and for the reason that helper's own comment gives — *a case that asked "what happens where there is no `lane`" against a workstation that has one would go green on the answer to a different question*. The codex case now builds its PATH the same way. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The codex-absent case is not asked, and the test file says why: this estate installs that launcher in /usr/bin, so "a PATH without codex" is a PATH without bash The round-3 fix at `lane-start:2132` stands — a launcher the table knows and this workstation does not have is refused in 5a, before the row, the object log and the Rule 3 stamp are written, rather than after `exec` exits 127 and leaves a false PAUSED→RESUMED transition in two append-only files. Its case cannot be asked here. `a17_path_without codex` is how the three `lane` cases build their PATHs, and on this workstation `codex` is `/usr/bin/codex`, `/bin/codex` AND `~/.npm-global/bin/codex` — the launcher is installed globally on every lane workstation — so subtracting every directory that holds it returns the EMPTY string and takes `bash`, `git`, `awk` and `sed` with it: the run then dies at its own shebang with `/usr/bin/env: 'bash': No such file or directory`. That is why the four assertions were green on every CI runner (which has no codex) and red at the desk, which is the exact failure mode the `a17_path_without_lane` comment exists to prevent. So the reason is written where the case would have been, with what a fixture would have to express to ask it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The merge takes main's openRepoShape pin, which `git add -A` had put back `git add -A` while resolving the merge staged the submodule at the commit this worktree happened to have checked out (`ba14e1d`, this branch's base), which reverted `main`'s own pin bump to `39d5c98` (#46) in the gitlink while `contracts/openreposhape-pin.yaml` carried main's. The pin file and the gitlink move in ONE commit and never apart — AGENTS.md rule 3 of the pinned standard — so the gitlink is put back where main has it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The re-mode case names the skill it actually chmods, because Amendment 17(a) renamed that act and the case came from a branch written before the rename Both Linux CI jobs went red at `5c162df` — the merge of `main` at `b5244e2` — on one assertion of `test_a_skill_whose_bytes_are_right_and_whose_mode_is_not_is_re_moded`, and the defect is a name spelled twice in one file. `skill_paths()` DERIVES its two paths from `SKILL_NAMES`, so on this branch it hands back `handoff`'s — Amendment 17(a) renamed the act from `lane-swap` to `handoff` and kept `lane-swap` as an alias file — while the case that #40 and #44 added on `main` asserts the printed line with the literal `lane-swap:`. The installer was right: it chmod'ed the path it was given and named the skill that owns it (`handoff: … (mode restored to 644)`). The merge that brought a case written before a rename on to the branch that made it is exactly where a spelled name goes stale, and the file's own header says so five lines above the constant it should have used. Both assertions take `{SKILL_DIR_NAME}` now, which is the same derivation the paths take. Merges `origin/main` at `a954435` (#50, `commit_push`'s conflict die and its `tests/test_lane_helpers.sh` § #32) on top of `b5244e2` (#44, the installer's mode stamping and unwritable-target refusal; #53's `skills/restart/SKILL.md`). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The `--in-process` help says what to EXPECT, `--late --in-process` is refused as the contradiction it is, and a comment stops claiming a refusal `canon_lane` does not make — Copilot rounds 4 to 6 Three findings taken; the other ten are #72, filed with what each would cost, because every one of them hardens an act that already refuses rather than fixing something that writes the wrong thing. * `lane-handoff:65` and its `--in-process` usage line still told the reader the top block would say *relaunch NONE of them* — the instruction Amendment 17 Addendum 1 (j) removed from the template two commits ago. `usage()` PRINTS that comment block, so the command was handing a person the opposite of the adopted rule while generating the right block. Both now say the block tells the next session what to EXPECT, and that what to DO is clause (i)'s count. * `--late --in-process` was not refused, and the kind block takes the `in-process` arm first — so a late record could have said `kind in-process` and told the next session to expect writers live that died with the process the record is about. A late record is written for a session that has ALREADY ENDED; that is what makes it late. * And the comment over the `canon-lane` read claimed it refuses "a `--lane` this estate has no name for". It does not: `canon_lane` answers with the TYPED spelling where no row matches, deliberately, because `lane-start` uses the same resolver for a lane it is about to create. What that read refuses is an AMBIGUOUS register (15(d)) and a value that is not a lane name at all; a well-formed lane with no row is named by the writes below, and the comment says so now rather than promising a check that is not there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The documented guard counts the way the wrapper counts, and `pytest_live` stops reporting a failure in the same breath as a zero — Copilot rounds 7 and 8 Three threads on one inconsistency and one on its neighbour, all of them this branch's own: * `AGENTS.md` and `docs/README-lanes.md` showed the guard with `pgrep -fc`, which `tests/run.sh` deliberately does NOT use — `-c` is not in every `pgrep` this repository runs under, and it is the COUNT and never `pgrep`'s exit status that decides. A document that shows a person a non-portable form of the very guard the wrapper exists to make portable is worse than one that shows none. Both snippets count with `awk` now, as the wrapper does, and both say the wrapper is the canonical implementation and why the lines are repeated at all. * `tests/run.sh:66` — `pgrep` exits 1 when it matches nothing and `set -o pipefail` makes that the pipeline's, so `pytest_live` printed `0` and returned a FAILURE together. Every caller today reads the printed count, which is what its own comment says; the explicit `return 0` is what keeps that true for the first caller that uses it in a condition. The two remaining findings of those rounds — parametrising the skill-placement assertions over `SKILL_NAMES`, and an `INSTALLABLES=( … )` regex that tolerates a wrapped array — are #72's kind and are noted there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The skill records the KIND its command records, and asks `window-session` the question that read answers — Copilot round 9 Two conformance gaps between the two halves of ONE act, which is the thing Amendment 17(a) exists to prevent: `skills/handoff/SKILL.md` is `/handoff` and `/ctx` for a session with no `lane-handoff` on `PATH`, so a fact the command writes and the skill does not is a record whose shape depends on which door the person came through. * `skills/handoff/SKILL.md:476` — the skill's `PAUSED` payload carried Amendment 17(b)'s two sub-fields and not Addendum 1 (h)'s `kind`. A record from the skill was therefore one no reader could tell a surviving writer from a dead one by, while the top block it writes in the same act states the kind. The payload gains `; kind <k>` and the line's free text gains the same word after the why — `clear in-process`, `clear respawn`, `kind unknown`. * `skills/handoff/SKILL.md:612` — the `/ctx` respawn asked `window-session` with `#{window_id}` alone (`@21`). That read matches a live record on the HARNESS's own `tmux` field, `<session>:<@id>`, and the three spellings of one window are never derived from each other — `lane-start:975-984` says so and `lane-handoff` reads it that way. The skill's `/ctx` therefore found no record, left `$pane` empty, and respawned something that was not the lane's pane. It asks with `#{session_name}:#{window_id}` now. Three cases in `tests/test_lane_helpers.sh` § "Amendment 17", beside the ones that already hold the skill to the command's words. DECLINED, from the same round: `lane-start:2177`, that `--agent claude` should skip the recorded transcript. Amendment 17(c) is explicit the other way — *"`claude` with its transcript here resumes by id exactly as Amendment 6 has always done; every other case launches the named agent with the handoff's top block as its first prompt"* — and the suite's `--agent claude with no transcript here` case is the branch that takes a new session. `LANE_START_FRESH=1` is the seam for *a new session even though the transcript is here*, and it is what `/ctx` sets. FILED: `lane-handoff:924`'s late-record race is #72's kind and is noted there — the preflight reads the lane's last line before the poll and the refresh, and a `RESUMED` appended in that window would be overtaken by the `PAUSED` this writes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The skill's row flip reads BOTH state-cell shapes, because an ordinary fresh row is the one it did not know — Copilot round 10 `| WORD ·` is a state cell with history after it and `| WORD |` is one with none, and a fresh row from `add-row` is the second. The skill's parser knew only the first, so for those rows `$state` was empty, `replace-in-row` had no anchor, and `/handoff` through the skill left the lane RUNNING in the register with its handoff already refreshed and its PAUSED line already written — the one state this act exists to make impossible. `lane-handoff` has read both shapes all along and keeps whichever punctuation the row carries; the skill does the same now, including in the replacement it hands the writer. Two cases in `tests/test_lane_helpers.sh` § "Amendment 17", beside this round's other two. The round's other finding — a multi-line `why` reaching `append-line` and the top block before the event writer validates it — is #72's finding 6, filed in round 4 and noted again there with what `append-line` adds to it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>



Lane: openRepoTools-3
Closes #32
commit_push's true-rebase-conflict die no longer says "nothing was pushed." Three of the five occurrences #32 records (Eagle, 2026-09-13/14, several lanes writing the register concurrently) named a commit that WAS on origin by the time anyone read the line — carried there by the very nextlanes-edit.shwrite out of the same shared checkout, which pulls the dangling local commit onto its own and pushes both together. A caller who trusted "nothing was pushed" and redid the write wrote a duplicate line. An aborted pull is not proof the commit never reached origin — the same rule RV-B3 took for the handoff stamp's own push inlane-start.The change (
lanes-edit.sh:1167–1181)rebase conflict on origin/<branch> — not pushed by this attempt; a later write from this checkout may already carry it, so look before you retry.Exit 3 is unchanged.git fetch origin <branch> && git log --oneline origin/<branch> -3; then — only if the commit is not there — thediffthat reads back what was written; then thereset --hard; then the redo. The old order putreset --hardsecond, before anyone had looked.Tests (
tests/test_lane_helpers.sh:3501–3601, 13 new assertions)A new
#32section. A second clone plays the peer that moves origin between this invocation's own commit and its push — the same device the lost-claim race above it uses. Its preamble explains why it first commits the sandbox checkout's leftover dirt from therejectgitcases: otherwisedirty_elsewherewould route these cases through Amendment 5(d)'s skip-the-pull branch instead of the rebase they exist to exercise.gitlands an unrelated peer commit in the exact window the retry loop exists for: after this attempt's ownpull --rebaseand just before its push. Asserts exit 0; the message namespushed origin/main (attempt 2)and never(attempt 1); and the row's text is in the register exactly once, never twice.REBASE CONFLICTis still named; the abort message appears andgit status --porcelainreally is empty; the fetch-and-look line, the "only if it is not there" gate and the attempt-scoped die are all present; the old unconditionalnothing was pushed.is absent; and the look is printed BEFORE thereset --hardit gates.Verification
bash -n lanes-edit.shandbash -n tests/test_lane_helpers.sh: clean.bash tests/test_lane_helpers.shon this branch's HEAD (8202742, with main8345d2fmerged in): 1691 passed, 0 failed, 0 skipped, 0 pending. Main alone is 1678; the 13 new assertions are the whole delta.python3 -m pytest tests -q: 571 passed in 1051.30s (0:17:31), serialized behind the shared pytest lock (sibling worktrees were testing concurrently).Lane: openrepotools-3 (openRepoTools-3)
🤖 Generated with Claude Code
Summary by Sourcery
Make rebase-conflict recovery distinguish an unpushed attempt from a commit that may already have reached origin through a later shared-checkout write.
Bug Fixes:
Enhancements:
Documentation:
Tests: