Skip to content

lane and lane-handoff refuse empty and flag-shaped flag values, and lane-handoff renames the window only once the handoff can no longer be refused (#80 slices B and C) - #220

Merged
brettheap merged 3 commits into
mainfrom
fix/handoff-flag-values-and-window-80
Oct 18, 2026
Merged

brettheap merged 3 commits into
mainfrom
fix/handoff-flag-values-and-window-80

Conversation

@brettheap

@brettheap brettheap commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Part of #80: slices B (flag values) and C (tmux and the window). Slice A, the two fixes to skills/handoff/SKILL.md (items 4 and 7), is deferred and remains open. Details under Remaining.

What was wrong

These are Copilot's findings on workBenches#91 against the vendored bytes. I checked each one at f87fd5a by running the new cases against that commit's code; the results are quoted below.

  • Item 1, lane:215. In lane --dir --all, --dir takes --all as its value. At f87fd5a this exits 0 with a checkout called --all, so the "every repository" listing the person asked for never runs. --force --dry-run has the same flaw: --dry-run becomes the why of a real takeover, and that why is written into another place's append-only log.
  • Items 6 and 8, lane-handoff:242 and :248. --lane= and --writers-file= accept an empty value. So do the other five = forms: --operation=, --at=, --agent=, --transcript= and --dir=. At f87fd5a all seven exit 0.
  • A gap under items 6 and 8. The spaced form never refused either. Every spaced arm reads x="$(flag_value …)", and a die inside $( ) only exits that subshell. The command prints REFUSED and then carries on with the value empty. At f87fd5a, lane-handoff --lane --restart clear exits 0 after this:
    lane-handoff: REFUSED: --lane needs a value and '--restart' is a flag. Use --lane=--restart where that really is the value.
    lane-handoff: lane=repoHF-4 window=hfsess:0 @21 workstation=Eagle
    
    It handed off whatever lane the window resolved to, as a plain handoff, because --restart had been consumed. --writers-file "" also exits 0, and writes a handoff with no briefs.
  • Item 10, lane-handoff:351. have_tmux checks only for the binary. Run from a shell outside tmux (env -u TMUX), f87fd5a renames the window and records window hfsess:0 @21, both read from whatever client the tmux server last saw.
  • Item 3, lane-handoff:434. *:[0-9]* and @[0-9]* are prefix globs. At f87fd5a, the append-only log takes window hfsess:1junk @21junk when the values come from tmux, and window hfsess:7junk @77junk when they come from the launcher's environment.
  • Items 2 and 11, lane-handoff:519. The rename ran before every remaining refusal: the --late preconditions, the writers' snapshot mktemp, the row read (sread register-row) and the --restart binding. Two of those refusals say "nothing was renamed". At f87fd5a a refused --late renames the window first (rename-window repoHF-7 is logged: before 1, after 2). A failed rename was also swallowed. With the fake tmux refusing the rename, f87fd5a exits 0 and writes the PAUSED record and the row anyway.

Already fixed on main (no change here):

What changed

  • lane: a new not_a_flag helper. The spaced --dir, --force and --wait arms now refuse a value that starts with -, exit 64, and name the = spelling (--dir=--all). Empty = values were already refused at each arm.
  • lane-handoff parser:
    • One arm, ahead of the seven =* arms, refuses --operation= | --at= | --lane= | --agent= | --transcript= | --writers-file= | --dir= with exit 2. Because case takes the first match, the =* arms still accept a value that starts with -. For example, --lane=-x still reaches the lane-name check.
    • Every spaced arm now ends || exit, so flag_value's refusal ends the command and keeps its own exit status.
    • A spaced flag as the last argument no longer makes the argument loop spin forever. This is lane-handoff only, in the seven spaced arms (--operation, --at, --lane, --agent, --transcript, --writers-file, --dir). At f87fd5a, lane-handoff --lane printed REFUSED from inside $( ), then ran shift 2 with one argument left. That shift fails and consumes nothing, so the loop never ended and printed REFUSED on every pass. At this head the same command exits 2, with one REFUSED. lane never had the loop: its spaced --force, --wait and --dir arms die in the main shell and exit 64.
  • have_tmux: now also requires $TMUX, the same test as lane's tmux_here. The header's environment line says so.
  • Window refs: window_ref_ok (^.+:[0-9]+$) and window_id_ok (^@[0-9]+$) are written in POSIX case, the way lane-start writes its id check. They are applied to the live tmux values and to the WORKBENCHES_CLAUDE_WINDOW_REF/_ID fallbacks.
  • The rename:
    • It moved to a new section 3a. That is after the handoff refresh and commit, which hold the last refusal (the --restart binding), and before section 4, where the PAUSED record is built and written.
    • A rename that tmux refuses now ends the command with exit 2 and tmux's own message. Nothing of the record is written, and no pane is touched. The refusal says what had already landed (the handoff's new top block, SWAPPING) and gives the re-run.
    • --late still renames, just before its own PAUSED, as it did before.
    • The header's exit-code-2 text describes this new case.

Functions and regions edited

At head 9f6fd2d, as old-line to new-line ranges against f87fd5a:

Conflict checks: I ran git merge-tree --write-tree of this head against every open PR head, #195-#218. All merge clean, including #218 (#85/#72, the why/sanitize edits). Lane 3's feat/triad-lane-tooling and feat/triad-lane-tooling-handoff also merge clean. Their nearest hunks are at old 1894, 1974-1996, 2057, 2217 and 2339. The removed block (old 1817-1825) and the new 3a (after old 2147) are 69 or more lines from any of them.

Tests

New cases in tests/test_lane_helpers.sh:

  • lane section: lane --dir --all gives 64 with the --dir=--all hint. --force --dry-run and --wait --switch give 64 in the same words. None of the three launches anything.
  • Amendment 17, part 3b:
    • All seven empty = forms give 2, each with its own flag named.
    • --lane --restart gives 2 with exactly one REFUSED and no lane resolved after it.
    • --writers-file "" gives 2 and never reaches the poll.
    • Across all of those, the logs, the register and its HEAD are byte-identical.
    • --lane=-repoHF-4 still reaches the lane-name check.
  • Amendment 17, part 3c:
    • A refused --late renames nothing.
    • With FAKE_TMUX_RENAME_FAIL=1 the result is 2, with tmux's words and the tmux rename-window act, the rename attempted once, no PAUSED and no row flip. A plain re-run then gives 0, renames, and writes one PAUSED and flips the row.
    • Run with env -u TMUX, the handoff gives 0, renames nothing, and records no window.
    • Malformed tmux refs are dropped for well-formed launcher fallbacks (; window hfsess:7 @77;).
    • Malformed fallbacks give no window sub-field.

Fail-before and pass-after, locally and in isolation. Per the lane's rule, I did not run the full suite locally. I built a trimmed copy of the new tests/test_lane_helpers.sh, containing the global setup, the lane fence cases and the whole Amendment 17 section up to its launcher table, plus the footer. I ran it under env -i HOME PATH LANG USER in two scratch trees that carry the same test file: git archive f87fd5a and this head's files.

code result
f87fd5a 161 passed, 41 failed. All 40 new-case failures are the defects above.
this head 201 passed, 1 failed

The single failure in both runs is an existing assertion that reads repoSW-1, a fixture seeded in a section the trim leaves out. It is not in a section this PR touches.

I tried to run the tests/test_supervised_restart_regressions.py cases that drive lane-handoff --restart through tests/run.sh … -k …. The wrapper waited out its 590-second bound on another lane's live pytest run, so those are left to CI, the suite of record.

bash -n passes on lane, lane-handoff and tests/test_lane_helpers.sh. Every construct used is bash 3.2-safe: case, ${x%=}, ${x##*:} and if ! v="$(…)".

Remaining

This is not a full fix for #80. Slice A is still open:

  • Item 4: the same prefix-glob window check in skills/handoff/SKILL.md's fenced block, at about lines 454-456.
  • Item 7: the literal <sanitized verbatim> / <what's open…> placeholders in that block's record writes, at about lines 547-552.

It is deferred because other writers are editing SKILL.md in that range.

🤖 Generated with Claude Code

Summary by Sourcery

Harden lane and lane-handoff argument and tmux handling so refused handoffs do not cause unintended changes or record inconsistent state.

Bug Fixes:

  • Reject empty flag values and flag-shaped values where lane or lane-handoff expect an argument, preventing unintended operations and ensuring invalid invocations stop immediately.
  • Prevent lane-handoff from acting on tmux state when run outside the current tmux client and ignore malformed window references.
  • Defer window renaming until all handoff refusal points have passed, and treat a failed rename as an error without writing the PAUSED record or updating the lane row.

Enhancements:

  • Strengthen validation of tmux window references and preserve support for explicitly supplied dash-prefixed values through equals-form flags.

Tests:

  • Add coverage for invalid lane and lane-handoff arguments, refusal side effects, tmux context handling, malformed window references, and rename failures.

…ane-handoff renames the window only once the handoff can no longer be refused (#80 slices B and C)

Copilot's review of workBenches#91 found these against the vendored bytes,
and each was still true at f87fd5a:

- `lane --dir --all` took `--all` as the checkout (item 1), and
  `--force --dry-run` ran a real takeover whose why was `--dry-run`. The
  three spaced value arms now refuse a `-`-led value with 64 and name the
  `=` spelling.
- `lane-handoff --lane=` and `--writers-file=` (items 6 and 8), and the other
  five `=` forms, took an empty value as no flag at all. One arm ahead of the
  seven now refuses it. Under them, the spaced form's refusal ran inside
  `$( )`, so it printed REFUSED and carried on: `--lane --restart` handed off
  the window's lane with exit 0. Every spaced arm now ends `|| exit`.
- `have_tmux` answered for any tmux server, inside a client or not (item 10).
  It now requires `$TMUX`, as `lane`'s `tmux_here` does.
- The window ref and id checks were prefix globs (item 3), so `s:1junk` and
  `@21junk` went into the append-only `window` sub-field. They are now
  whole-string matches, for tmux's answers and the launcher's fallbacks.
- The window rename ran before the `--late` preconditions, the snapshot
  file, the row read and the `--restart` binding, and its failure was
  swallowed (items 2 and 11). It now runs after the last refusal and before
  the PAUSED record, and a refused rename is a refusal that names what had
  already landed.

The suite's fake tmux gains `$FAKE_TMUX_RENAME_FAIL`. Slice A, the two
fixes in skills/handoff/SKILL.md (items 4 and 7), is deferred.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR hardens lane and lane-handoff argument parsing, validates tmux context and window metadata, and moves window renaming to the final non-refusable point so failed or refused handoffs do not produce misleading window state or PAUSED records. It includes focused regression tests for all affected behaviors; the documented SKILL.md fixes remain deferred.

File-Level Changes

Change Details Files
Reject ambiguous or empty flag values before they can be interpreted as operands or silently ignored.
  • Added dash-prefixed value detection for spaced lane options with exit-64 guidance to use =.
  • Added a centralized empty-= validation arm for the seven lane-handoff value options.
  • Propagated spaced-value refusal failures out of command substitution so parsing stops immediately and preserves side-effect-free behavior.
lane
lane-handoff
tests/test_lane_helpers.sh
Restrict tmux interaction and window metadata to the current shell and fully valid window references.
  • Required TMUX in have_tmux before querying or renaming a window.
  • Added whole-string validation for tmux window references and IDs, including launcher-provided fallbacks.
  • Covered malformed live and fallback values, and execution outside tmux, with regression tests.
lane-handoff
tests/test_lane_helpers.sh
Defer window renaming until all handoff refusal points have passed and make rename failures transactional.
  • Moved the rename after handoff refresh/commit and the final restart binding, immediately before PAUSED record creation.
  • Propagated tmux rename failures as exit 2 with tmux diagnostics and avoided writing the PAUSED record or flipping the row.
  • Retained the --late rename behavior while ensuring refused operations never rename the window.
lane-handoff
tests/test_lane_helpers.sh
Add focused regression coverage for flag parsing, tmux context, window validation, and rename ordering.
  • Added fake-tmux support for deterministic rename failure simulation.
  • Verified empty and flag-shaped values produce no writes or lane resolution.
  • Verified refused and failed renames, outside-tmux handoffs, malformed references, and successful retry behavior.
tests/test_lane_helpers.sh

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

…I tests this PR on today's main

Main moved 15 commits past this branch's base f87fd5a, through the suite's machinery and pin (.github/workflows/tests.yml, contracts/openreposhape-pin.yaml, tests/conftest.py, tests/test_repo_hygiene.py, upstream/openRepoShape), after this PR's last green run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…I tests this PR on today's main

Main moved 4 commits past this branch's base 2cb14a9, through the suite's machinery and pin (), after this PR's last green run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

brettheap added a commit that referenced this pull request Oct 18, 2026
…ane holds the object (#74) (#204)

Landed on Brett Heap's word "land #204 #206 #208 #218 #220 #221 when green" (2026-10-18), by lane openRepoTools-5. Required Linux CI (guard-launch-mode, tests, tests-no-submodule; run 39137725493) was green on head af5b7ce with main 586dea2 in its tree.

Independent review (lane 5, Opus, of 4996156; the merges of main add nothing to the PR's diff): LAND.

Closes #74

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…so a migrated PAUSED row stops showing NO LOG (#89) (#206)

Landed on Brett Heap's word "land #204 #206 #208 #218 #220 #221 when green" (2026-10-18), by lane openRepoTools-5. Required Linux CI (guard-launch-mode, tests, tests-no-submodule; run 39137733527) was green on head cd3ced0 with main 586dea2 in its tree. Main 0a49697 merged with head cd3ced0 is tree f88c2f1, which queue run 39137981522 (workflow_dispatch of tests on 3f5f4b9) tested whole and green.

Independent review (lane 5, Opus, of 00b75e6; the merges of main add nothing to the PR's diff): LAND. Brett Heap confirmed the unpickable (row) lanes as built (2026-10-18).

Closes #89

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…or binding log instead of guessing a branch (#60) (#208)

Landed on Brett Heap's word "land #204 #206 #208 #218 #220 #221 when green" (2026-10-18), by lane openRepoTools-5. Required Linux CI (guard-launch-mode, tests, tests-no-submodule; run 39137741203) was green on head 3edaf0a with main 586dea2 in its tree. Main be82c20 merged with head 3edaf0a is tree cfd2ee6, which queue run 39137988005 (workflow_dispatch of tests on 9db4863) tested whole and green.

Independent review (lane 5, Opus, of c252a3c; the merges of main add nothing to the PR's diff): LAND.

Closes #60

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…s why refuse it before writing, and the deferred lane-start command keeps --agent (#85, #72 items 5, 6) (#218)

Landed on Brett Heap's word "land #204 #206 #208 #218 #220 #221 when green" (2026-10-18), by lane openRepoTools-5. Required Linux CI (guard-launch-mode, tests, tests-no-submodule; run 39137747959) was green on head bfc5ddd with main 586dea2 in its tree. Main fca60f5 merged with head bfc5ddd is tree b188309, which queue run 39137992777 (workflow_dispatch of tests on 586df3b) tested whole and green.

Independent review (lane 5, Opus, of 99f3853; the merges of main add nothing to the PR's diff): LAND. Brett Heap confirmed refusing CR/LF at the door (2026-10-18).

Part of #85. Part of #72 (items 5-6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap
brettheap marked this pull request as ready for review October 18, 2026 15:59
@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

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 9 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit be0743b into main Oct 18, 2026
5 checks passed
brettheap added a commit that referenced this pull request Oct 18, 2026
…ession ids, so the dead-lane --force check and duplicate-holder fail closed (#67) (#221)

Landed on Brett Heap's word "land #204 #206 #208 #218 #220 #221 when green" (2026-10-18), by lane openRepoTools-5. Required Linux CI (guard-launch-mode, tests, tests-no-submodule; run 39137764839) was green on head 14696c7 with main 586dea2 in its tree. Main be0743b merged with head 14696c7 is tree 0052eff, which queue run 39138003629 (workflow_dispatch of tests on 147cbf6) tested whole and green.

Independent review (lane 5, Opus, of fbd2bb1; the merges of main add nothing to the PR's diff): LAND.

Closes #67

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
… this PR on today's main

One conflict, in tests/test_lane_helpers.sh, in the FAKE TMUX header: #220's
paragraph on FAKE_TMUX_RENAME_FAIL (its code is the rename-window arm below,
which merged cleanly) and this PR's FAKE_TMUX_SILENT paragraph and block both
insert directly before `case "${1-}" in`. Resolved as #220's independent review
asked: #220's FAKE_TMUX_RENAME_FAIL comment first, then a `#` separator, then
this PR's FAKE_TMUX_SILENT comment and block, both before the `case`. They do
not interact: the silent-server exit answers before any arm runs, and the
rename refusal lives in the rename-window arm. Every line of both sides is kept.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant