Skip to content

The handoff skill's step-1 fence refuses Amendment 15(d)'s exit 2 instead of reading it as no answer, and its window ref is checked as a whole string (#72 item 8, #80 item 4) - #260

Merged
brettheap merged 2 commits into
mainfrom
fix/handoff-skill-exit2-and-window-72-80
Oct 19, 2026
Merged

brettheap merged 2 commits into
mainfrom
fix/handoff-skill-exit2-and-window-72-80

Conversation

@brettheap

@brettheap brettheap commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

What was wrong

#72 item 8: the skill read every exit 2 as "no answer". Step 1 of skills/handoff/SKILL.md reads the register through sread, and its arm at 08e7550 was:

    8|2) sr_out="" ;;

But exit 2 has two meanings. It is what a lanes-edit.sh predating a subcommand returns for an unknown one. It is also canon_lane's refusal of a register holding two rows that differ only by case (Amendment 15(d)), which register-row carries (lanes-edit.sh's register-row) arm: lane="$(canon_lane "$lane")" || exit 2). Falling through that refusal goes to the next rung. The last rung is the workstation's newest swap, a different lane, and the skill then renames the window for it and writes a PAUSED record and a restart command about it. lane-handoff's own sread already tells the two apart by the helper's words (its 2) arm, taken in Copilot round 2 on #47). The skill had not caught up.

Measured against 08e7550 with a stub helper: window repo-1, register-row exits 2 with Amendment 15(d)'s words, window-lane exits 8, and swapped answers other-7. Step 1 then printed lane=other-7.

#80 item 4: the window ref was checked by a prefix glob. Step 4 held the live #S:#I and the launcher's WORKBENCHES_CLAUDE_WINDOW_REF to case "$win" in *:[0-9]*). So s:1junk, :1, and a value with a newline after the index all went into the append-only window sub-field. window-lane can never resolve them there, because a live #S:#I always ends in an index, which is digits.

Fix round (the independent review of c9d681f). The first commit left two gaps.

  • Rungs 2 and 3 were not read through the resolver. Step 1 refused a case-twin lane only when rung 1 found it, because only rung 1 reads its name through register-row, and so through canon_lane. Rungs 2 and 3 answer with a record's lane field and the swap row's first cell, and nothing read either through the resolver. The reviewer's case: window zsh, register-row exits 8, swapped names other-7, and the register holds other-7 and Other-7. At c9d681f, step 1 printed lane=other-7 and exited 0. lane-handoff closes this with sread lane_canon … canon-lane "$lane" after its rungs.
  • A line break before the index passed the new regex. The . in ^.+:[0-9]+$ matches a line break, so junk<LF>s:1 passed. log PAUSED refuses a payload carrying a newline or a carriage return, so such a value costs the whole record, not just the sub-field.

What changed

  • sread gains a 2) arm. It asks the same read again with only stderr kept, as lane-handoff does. If those words contain Amendment 15(d), it prints a REFUSED: line naming the read, relays the helper's words, and stops the skill with exit 2. Any other exit 2 falls through with the answer empty, as before. 8, 0 and every other code behave as before.
  • The fence's comment now says that one 2 is a refusal, and gains the "2 is two answers" paragraph that matches lane-handoff's. The comment calling the fence /restart's lread "with one difference" now names the second difference: lread still reads every 2 as no answer.
  • Step 1 reads the bound name through canon-lane (fix round). Whichever rung answered, the name goes through the same sread, as lane-handoff does:
    • Amendment 15(d)'s 2 refuses.
    • An old helper's 2 keeps the name.
    • A read that failed stops the skill. This includes 66, an alias table that could not be read, which canon-lane passes through rather than flattening to 2.
    • The register row's spelling is the name used for the rename, the record and the restart line.
    • One sentence under the block says so.
  • Both window checks are now [[ "$win" =~ ^.+:[0-9]+$ ]] || win="". This is the form the <@id> check beside them already uses, and the same rule as lane-handoff's window_ref_ok. In the fix round, each check also gets case "$win" in *$'\n'*|*$'\r'*) win="" ;; esac, which works in bash and zsh. A comment above the block says why. A live value that is dropped falls back to the export, as an empty one did.

No other test or doc quotes the changed lines. The shell suite's sfence_probe and skill_win_of cut these blocks by awk ranges, and both ranges still match.

Functions and regions edited

  • skills/handoff/SKILL.md: the step-0 sh block, its comment above sread() and sread() itself (lines 62-116 at c249ba6)
  • skills/handoff/SKILL.md: step 1's block, a new canon-lane read after the three rungs (lines 203-219), and the prose paragraph under the block (lines 225-228)
  • skills/handoff/SKILL.md: step 4's window sub-field block: the comment above win="$(tmux display-message -p '#S:#I', the two shape checks and the two line-break checks (lines 539-555)
  • tests/test_handoff_skill_72_80.py: new file

This keeps out of #207's lines (SKILL.md 52-58, 126, 235-301 and 551-594 at 08e7550; this PR's hunks at 08e7550 numbering are 62-85, 177-183 and 493-497). git merge-tree --write-tree of c249ba6 is clean with #207's head 51909fe and with origin/main 9b94a59.

Tests

New file tests/test_handoff_skill_72_80.py (53 cases). It finds the blocks with test_skill_shell_blocks.py's own fenced_blocks, cuts them by the shell suite's ranges, and runs them under bash and zsh -f. lanes-edit.sh is a stub on a private PATH, tmux is a shell function, and the environment is an explicit dict, so no LANES_*, TMUX* or WORKBENCHES_* value is inherited.

  • The fence: exit 2 with the 15(d) words stops the skill. Nothing after the read runs, the read is named and the helper's words are relayed. Both calls are logged with the same arguments and LANES_NO_FETCH=1. An old helper's 2, a silent 2, 8, 0 and a failed read each behave as before.
  • Step 1 end to end:
    • Over a case-twin row, rung 1 stops before window-lane or swapped is asked.
    • Fix round: a case twin bound by rung 2 (window-lane) or rung 3 (swapped) refuses at canon-lane.
    • Fix round: canon-lane's other answers: the row's spelling is taken; a failed read (66) stops the skill; the same name, or an old helper's 2, keeps the lane.
    • With an old helper's 2 throughout, step 1 still reaches the newest swap.
  • The window ref:
    • s:1junk (live and exported), a rejected live value falling back to the export, :1, a newline after the index, s:1:2, s: and @12junk for the id.
    • Fix round: a newline before the index (exported, and live falling back to the export), and a carriage return.

Fail-before and pass-after, locally. Each run used flock … python3 -m pytest, with every LANES_*, TMUX* and WORKBENCHES_* unset.

SKILL.md at failed passed which failed
08e7550 (main) 33 20 all 33 RED cases
c9d681f (this PR's first commit) 18 35 exactly the 18 fix-round cases: rungs 2 and 3, four canon-lane answers, three line-break refs, each under bash and zsh
c249ba6 (this head) 0 53 none

Also at c249ba6:

  • tests/test_skill_shell_blocks.py, tests/test_repo_hygiene.py and tests/test_handoff_skill_commit_block.py: 233 passed.
  • The shell suite's own sfence_probe and skill_win_of cases (tests/test_lane_helpers.sh, the lines that read these blocks, run on their own): 18 passed, 0 failed.
  • bash -n and zsh -n pass on all three edited blocks.

Left as they were

Part of #72 (item 8)
Part of #80 (item 4)

🤖 Generated with Claude Code

…tead of reading it as no answer, and its window ref is checked as a whole string (#72 item 8, #80 item 4)

#72 item 8: `sread`'s `8|2) sr_out=""` arm read every exit 2 as "no answer"
and fell to the next rung. But 2 is also `canon_lane`'s refusal of a register
holding two rows that differ only by case (Amendment 15(d)), which
`register-row` carries. Falling through it bound the window to whatever the
next rung answered, and the last rung is the workstation's newest swap: a
different lane, which the skill would then rename the window for and write a
PAUSED record and a restart command about. `lane-handoff`'s own `sread` already
tells the two apart by the helper's words. The skill now does the same: a 2
whose stderr carries "Amendment 15(d)" stops the skill (exit 2) and relays the
helper's words, and any other 2 still falls through, as from an old helper.

#80 item 4: step 4 held the live `#S:#I` and `WORKBENCHES_CLAUDE_WINDOW_REF`
to `case "$win" in *:[0-9]*)`, a prefix glob, so `s:1junk` (or `:1`, or a
value with a newline after the index) went into the append-only `window`
sub-field, where `window-lane` can never resolve it. Both checks are now
`[[ "$win" =~ ^.+:[0-9]+$ ]] || win=""`, the form the `<@id>` check beside
them already uses, and the same rule as `lane-handoff`'s `window_ref_ok`.

tests/test_handoff_skill_72_80.py extracts both blocks from the skill and runs
them under bash and zsh -f with a stub lanes-edit.sh and a tmux function. 15 of
its 35 cases fail against 08e7550's skill and all 35 pass with this change.

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

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR hardens the handoff skill by stopping safely on Amendment 15(d)'s ambiguous-register refusal while preserving legacy exit-2 compatibility, and by enforcing whole-string validation for live and exported tmux window references; the new 35-case shell tests exercise both fixes under Bash and zsh.

Flow diagram for handoff read refusal handling

flowchart TD
    A["sread invokes lanes-edit.sh"] --> B{"Exit status?"}
    B -->|0| C["Use answer"]
    B -->|8| D["Treat as no answer and continue"]
    B -->|2| E["Read stderr again"]
    E --> F{"Contains Amendment 15(d)?"}
    F -->|Yes| G["Print REFUSED and exit 2"]
    F -->|No| D
    B -->|Other| H["Print REFUSED and exit 1"]
Loading

Flow diagram for whole-string window reference validation

flowchart TD
    A["Read live tmux window ref"] --> B{"Matches ^.+:[0-9]+$?"};
    B -->|Yes| D["Use live ref"];
    B -->|No| C["Discard live ref"];
    C --> E["Read WORKBENCHES_CLAUDE_WINDOW_REF"];
    D --> F{"Validate selected ref"};
    E --> F;
    F -->|Valid| G["Use window ref"];
    F -->|Invalid| H["Clear window ref"];
Loading

File-Level Changes

Change Details Files
Distinguish Amendment 15(d) refusals from legacy helper exit 2 responses in the handoff step-1 read fence.
  • Retry exit-2 reads with stderr captured to identify the Amendment 15(d) diagnostic.
  • Emit a refusal and terminate with exit 2 when the diagnostic indicates case-colliding register rows.
  • Preserve existing fall-through behavior for old, silent, and unknown-subcommand exit 2 responses.
  • Add focused Bash and zsh coverage for refusal handling, unchanged exit behavior, and step-1 rung short-circuiting.
skills/handoff/SKILL.md
tests/test_handoff_skill_72_80.py
Validate tmux and exported Claude window references as complete session-and-index strings rather than prefix-matching them.
  • Replace prefix globs with the whole-string regex ^.+:[0-9]+$ for both live and exported window references.
  • Retain fallback from an invalid live reference to the exported reference.
  • Add coverage for trailing junk, missing session or index, newlines, multiple colons, and valid references.
skills/handoff/SKILL.md
tests/test_handoff_skill_72_80.py

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

…so a case twin from window-lane or the newest swap refuses too, and a window ref carrying a line break is dropped (#72 item 8, #80 item 4, review of #260)

The independent review of #260 measured the gap c9d681f left. Step 1 refused
a case-twin lane only where rung 1 found it, because only rung 1 reads its
name through `register-row` and so through `canon_lane`. Rungs 2 and 3 answer
with a record's lane field and the swap row's first cell, which nothing read
through the resolver. With window `zsh`, `register-row` 8, `swapped` naming
`other-7` and the register holding `other-7` and `Other-7`, step 1 printed
`lane=other-7` and exited 0. `lane-handoff` closes this by reading the bound
name through `canon-lane` after its rungs. The skill now does the same through
its fence: Amendment 15(d)'s 2 refuses, an old helper's 2 keeps the name, a
failed read (66, an alias table that could not be read) stops the skill, and
the register row's spelling is taken.

The window ref's regex `^.+:[0-9]+$` lets `.` match a line break, so
`junk<LF>s:1` passed it, and `log PAUSED` refuses a payload carrying a newline
or a carriage return. A value with either is now dropped beside each regex
check, in a `case` that works in bash and zsh.

The comment that called the fence `/restart`'s `lread` with one difference now
names the second (the 15(d) refusal), and SonarCloud's two composite asserts
in the test are split.

tests/test_handoff_skill_72_80.py gains 18 cases (rungs 2 and 3, canon-lane's
answers, three line-break refs). All 18 fail against c9d681f, 33 of the 53
fail against 08e7550, and all 53 pass with this change, under bash and zsh -f.

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 19, 2026
… cause and both documented rows, so a die that claims nothing was pushed or a fourth die no row documents fails it (#68 item 5) (#259)

Landed on Brett Heap's word "land #259 #260 #261 when green" (2026-10-19), by lane openRepoTools-5. Required Linux CI (guard-launch-mode, tests, tests-no-submodule; run 39171682413) was green on head adb6940 with main 08e7550 in its tree. Main 5e88a2c merged with head adb6940 is tree afe4dc7, which queue run 39248744253 (workflow_dispatch of tests on fb9c0d8) tested whole and green.

Independent review (lane 5, Opus, of 25ae36d): LAND. Fix round 1 (adb6940) took its non-blocking points: the timeout-call reader reads bare and quoted operation words, the exit-3 die pattern takes any spacing, and the anchor wording is exact.

Part of #68

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

sourcery-ai Bot commented Oct 19, 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 2 days and 6 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit add5c15 into main Oct 19, 2026
5 checks passed
brettheap added a commit that referenced this pull request Oct 19, 2026
…, and prints both in its transcript and in CI's step summary (#77, step 1a) (#261)

Landed on Brett Heap's word "land #259 #260 #261 when green" (2026-10-19), by lane openRepoTools-5. Required Linux CI (guard-launch-mode, tests, tests-no-submodule; run 39245315763) was green on head d05dc0b with main 08e7550 in its tree. Main add5c15 merged with head d05dc0b is tree ca3c9d5, which queue run 39248755273 (workflow_dispatch of tests on 800ec88) tested whole and green.

Independent review (lane 5, Opus, of 6335ca8): LAND. Fix round 1 (d05dc0b) reports the teardown once and names a failed removal, and moves the rows-equal-headings check to the report.

Part of #77

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 19, 2026
…tized as lane-handoff's sanitize does, and a why carrying a line break is refused before step 1 (#80 item 7, #85)

This commit and the WIP checkpoint before it (520b98f) are one slice. Main
b707bd7 is merged in, so #260's sread 15(d) arm, its canon-lane read after
step 1's rungs and its whole-string window checks sit in front of step 4's
writes, unchanged.

#80 item 7: step 4 of skills/handoff/SKILL.md computed why_text and then
wrote neither record with it. The object log's PAUSED free text was the
literal "on <operator>'s word: <sanitized verbatim>", and the register's
file-level line carried that and "<what's open, or NOTHING CLAIMED>", so
running the block as printed recorded the placeholders. Both writes now
carry $why_text, and the register line is lane-handoff's own line word for
word, which is what "the same act writes the same record" (Amendment 17(a))
asks. That drops the "<what's open, or NOTHING CLAIMED>" segment, which
lane-handoff's line never carried.

#85, the skill's half: #218 made append-line and lane-handoff refuse a why
carrying CR or LF before anything is written, and made lane-handoff's
sanitize spell every break "; ". The skill did neither. Its why_text was
"$why" as typed, so it did not even replace " — ", though its own prose says
it does. A why with a break would reach step 2's top block, and then both
step 4 writers would refuse it after step 2 had already committed the
handoff. The skill now refuses such a why before step 1, with the words
lane-handoff uses at its own door. Step 4 then sanitizes the why as
lane-handoff's sanitize does (CRLF, CR and LF each one "; ", and every
" — "), with an empty why written "handoff". That is the second line of
defence.

tests/test_handoff_skill_80_85.py runs the door block and step 4's block
under bash and zsh -f against a stub helper. It compares the skill's two
writes with what lane-handoff's own sanitize, kind-word lines and
register-line template produce, all cut out of lane-handoff. This commit
makes the door-position case read only the lines that run. After the merge,
#260's comments in the preamble's fence cite lane-handoff by name, and a
comment that names an act is not that act.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 19, 2026
Main now holds lane 4 queue, lane 5 queue (#259 #260 #261) and #246 (55aae16).
The one conflict is tests/test_repo_hygiene.py import block: #262 adds
import sys and #259 adds from collections import namedtuple; both are kept.
The tree is lane3-queue/q1d-228pr1-on-p246c, run 39256880821, green.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 19, 2026
Main moved past this branch's base 5e88a2c: #259, #260, #261 and #246.
#246 touches lanes-edit.sh and docs/README-lanes.md too. The merge is
clean, the lock lines are main's own, and lanes-edit.sh parses under
bash 3.2.57 and 5.2. CI tests the moved main.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant