Skip to content

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

Merged
brettheap merged 6 commits into
mainfrom
fix/commit-push-reports-last-attempt
Sep 14, 2026
Merged

brettheap merged 6 commits into
mainfrom
fix/commit-push-reports-last-attempt

Conversation

@brettheap

@brettheap brettheap commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

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 next lanes-edit.sh write 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 in lane-start.

The change (lanes-edit.sh :1167–1181)

  • The die now reads 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.
  • RECOVERY is reordered to LOOK first: git fetch origin <branch> && git log --oneline origin/<branch> -3; then — only if the commit is not there — the diff that reads back what was written; then the reset --hard; then the redo. The old order put reset --hard second, before anyone had looked.
  • The comment above it (:1167–1174) names the mechanism and RV-B3 so the next reader does not put "nothing was pushed" back.

Tests (tests/test_lane_helpers.sh :3501–3601, 13 new assertions)

A new #32 section. 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 the rejectgit cases: otherwise dirty_elsewhere would route these cases through Amendment 5(d)'s skip-the-pull branch instead of the rebase they exist to exercise.

  • RACE-RETRY-WINS (:3534–3564) — a fake git lands an unrelated peer commit in the exact window the retry loop exists for: after this attempt's own pull --rebase and just before its push. Asserts exit 0; the message names pushed origin/main (attempt 2) and never (attempt 1); and the row's text is in the register exactly once, never twice.
  • OURS-CONFLICT (:3566–3601) — the peer rewrites the SAME row, a conflict the rebase truly cannot resolve. Asserts exit 3; REBASE CONFLICT is still named; the abort message appears and git status --porcelain really 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 unconditional nothing was pushed. is absent; and the look is printed BEFORE the reset --hard it gates.

Verification

  • bash -n lanes-edit.sh and bash -n tests/test_lane_helpers.sh: clean.
  • bash tests/test_lane_helpers.sh on this branch's HEAD (8202742, with main 8345d2f merged 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:

  • Prevent exit-3 rebase-conflict handling from falsely claiming that the commit was never pushed, avoiding duplicate writes when a later shared-checkout write has already carried it to origin.

Enhancements:

  • Reorder conflict recovery to fetch and verify the published change by patch content before reading back, resetting, or retrying.
  • Expand exit-code documentation to describe the uncertainty and all relevant causes of exit 3 consistently across the script and user documentation.

Documentation:

  • Document shared-checkout propagation, fetch requirements, and content-based recovery for aborted rebase conflicts.

Tests:

  • Add race and true-conflict coverage for retry behavior, recovery ordering, duplicate prevention, clean aborts, and synchronized exit-code documentation.

brettheap and others added 2 commits September 14, 2026 19:48
…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>
Copilot AI lite review requested due to automatic review settings September 14, 2026 20:32

@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 1 day and 4 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

Updates commit_push true-conflict messaging and recovery so an aborted pull is not treated as proof that the commit never reached origin, then adds race-focused tests covering both successful retry propagation and genuine same-row conflicts.

Sequence diagram for shared-checkout conflict recovery

sequenceDiagram
    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
Loading

Flow diagram for true-conflict recovery decision

flowchart 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
Loading

File-Level Changes

Change Details Files
Make true rebase-conflict recovery acknowledge that a later shared-checkout write may have propagated the commit.
  • Replace the misleading “nothing was pushed” failure text with an attempt-scoped warning and explicit look-before-retry guidance.
  • Document the shared-checkout propagation mechanism and RV-B3 rationale.
  • Reorder recovery instructions to fetch and inspect origin before reading the diff, resetting, or redoing the edit.
lanes-edit.sh
Add regression coverage for retry races and unrecoverable same-row conflicts.
  • Simulate a peer landing a commit between pull and push, verifying retry success, attempt-accurate messaging, and no duplicate row.
  • Simulate a peer rewrite of the same row, verifying exit 3, clean abort state, look-before-reset guidance, and removal of the old claim.
  • Normalize fixture checkout state so the scenarios exercise the intended rebase paths.
tests/test_lane_helpers.sh

Assessment against linked issues

Issue Objective Addressed Explanation
#32 Ensure lanes-edit.sh reports the result of the final commit/push attempt, including identifying which retry successfully pushed, so callers do not incorrectly retry and create duplicate entries. ✅
#32 When a genuine rebase conflict aborts the operation, leave the shared checkout clean and report that the commit was not pushed by this attempt without making the unsupported claim that it was never pushed; provide recovery guidance to check the remote before retrying. ✅
#32 Add regression coverage using a concurrent sandbox clone for both a retry that eventually succeeds and a genuine same-row rebase conflict, including verification of the exit status, messages, checkout cleanliness, and absence of duplicate writes. ✅

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

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 diff line. 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 existing lane-start recovery 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.

Comment thread lanes-edit.sh
…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
Copilot AI review requested due to automatic review settings September 14, 2026 21:18
sourcery-ai[bot]
sourcery-ai Bot previously approved these changes Sep 14, 2026

@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.

Sourcery assessment

Approved.

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

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 die at all; it only checks the table and manual. If the runtime message at lanes-edit.sh:1183 regresses to nothing was pushed while 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

Comment thread lanes-edit.sh Outdated
Comment thread docs/README-lanes.md Outdated
…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

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

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_die at line 1002 and the blocked-peer exhaustion at line 1140 also call die ... 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

Comment thread lanes-edit.sh Outdated
Comment thread docs/README-lanes.md Outdated
… 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
Copilot AI review requested due to automatic review settings September 14, 2026 22:10

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

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 say not-there and 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_die is invoked for git pull --rebase at 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

Comment thread lanes-edit.sh Outdated
Comment thread docs/README-lanes.md Outdated
Comment thread lanes-edit.sh Outdated
… 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
Copilot AI review requested due to automatic review settings September 14, 2026 22:39
@sonarqubecloud

Copy link
Copy Markdown

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.

🔵 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_REPO is 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 workspace makes the fetch/cherry command fail and prevents the required lookup. Emit a shell-quoted -C path 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, then git log ... -3 and git 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 cherry check: 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 as git 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_sha walks 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 permits reset/redo and can recreate the duplicate this change is meant to prevent. Limit the comparison to $cp_sha (for example, pass ${cp_sha}^ as git 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 cherry lookup against the peer-rebased and local-only histories described in the surrounding comment. A regression such as using the wrong git cherry range or treating a lookup error as 0 would 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 die and checks one shared phrase. Exit 3 is also produced by git_timeout_die and by the six-attempt peer-file branch, yet the later assertions merely look for timeout, push, pull, and peer's in 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

@brettheap
brettheap merged commit a954435 into main Sep 14, 2026
7 checks passed
@brettheap
brettheap deleted the fix/commit-push-reports-last-attempt branch September 14, 2026 23:17
brettheap added a commit that referenced this pull request Sep 14, 2026
…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
brettheap added a commit that referenced this pull request Sep 14, 2026
…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 added a commit that referenced this pull request Sep 14, 2026
…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
brettheap added a commit that referenced this pull request Sep 15, 2026
…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>
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