Skip to content

A one-line record can't take a newline: append-line and lane-handoff's why refuse it before writing, and the deferred lane-start command keeps --agent (#85, #72 items 5, 6) - #218

Merged
brettheap merged 3 commits into
mainfrom
fix/one-line-writes-and-deferred-agent-85-72
Oct 18, 2026
Merged

brettheap merged 3 commits into
mainfrom
fix/one-line-writes-and-deferred-agent-85-72

Conversation

@brettheap

@brettheap brettheap commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What was wrong

#85, and #72 item 6: a newline reached a one-line record. lanes-edit.sh's append_text_line appended its text with >> and only then proved the file had grown by one line. The append-line verb had no guard of its own, so a text carrying a newline was written whole and refused afterwards (exit 5, append changed line count by 2). write_event already refused a newline before writing (the R21 guard), so the object log was safe and the register was not.

lane-handoff puts its why into four places, and sanitize (which only replaced —) covers only the last two:

  1. the handoff's top block,
  2. the handoff commit's message,
  3. the object log's PAUSED line,
  4. the register's file-level PAUSED line.

To measure it, I ran the suite's own sandbox (a trimmed tests/test_lane_helpers.sh: setup plus the Amendment 17 section) against main f87fd5a with lane-handoff --lane repoHF-7 "$(printf 'clear\nsecond line')":

DBG rc=0
DBG commit: 844a494 LANES(repoHF-7@Eagle): state · PAUSED · swap; window testsess:0 @1; dir …; agent claude; ...
DBG commit: c477c93 LANES(pre-existing@Eagle): capture an uncommitted registry edit (row append)
DBG commit: c6151fa handoff(repoHF-7@Eagle): PAUSED, clear second line
DBG LANES.md tail:
DBG   | PAUSED — lane repoHF-7, session a17a0004-…@Eagle, 2026-10-13T18:35:18Z, clear
DBG   | second line kind unknown; handoff refreshed
DBG handoff head:
DBG   | ## RESUME PROMPT — PAUSED 2026-10-13T18:35:18Z (clear
DBG   | second line), agent claude, transcript a17a0004-…, kind unknown — …
DBG err: lane-handoff: the PAUSED line was NOT written — the helper's own refusal is above. …
DBG err: lane-handoff: the LANES.md file-level PAUSED line was not appended (the register may be busy or behind); …

So the act exited 0, and this is worse than the issue described:

  • The handoff was committed with a top block split across two lines. A --- line in the why would also end the first prompt early, because that is where lane-start's handoff_top_block stops.
  • The object log's PAUSED line was refused.
  • append-line wrote two lines. The row flip right after it did not refuse the dirty register: its handle_preexisting committed those two lines as a peer's pre-existing edit.
  • lane-handoff's own note then said the line "was not appended".

A carriage return was not refused anywhere on that path. wc -l does not count one, so append-line "LANDING — …\r…" was appended and committed with exit 0.

#72 item 5: the deferred lane-start command dropped --agent. Where no transcript uuid is knowable, lane-start prints the first act that will write the deferred records: $prog --no-launch $self_args. $self_args is only the lane. A codex launch never has a uuid here, so it always reaches that line, and the printed command dropped --agent codex. The person who ran it then had lane-agent choose the agent of the last PAUSED. For repoAG-4 in the suite that is claude. Measured at f87fd5a: lane-start: lane-start --no-launch repoAG 4.

One policy across #85 and item 6, and why

The writers refuse before they write, a typed why is refused at the door, and sanitize flattens as the second line of defence.

I chose refusing over flattening at the door because the person sees their own words refused with the reason and re-runs, and nothing is half-written. Silent flattening would also have worked; the trade is one re-run against the record differing from what was typed.

Note: lane-handoff has no --why flag. The why is the positional [why …], and the tests drive it that way.

What changed

  • lanes-edit.sh
    • append_text_line refuses CR/LF before mktemp (exit 2, "Nothing was written").
    • The append-line arm refuses CR/LF before the lock (exit 2, "Nothing was written, and the lock was not taken").
  • lane-handoff
    • A why carrying CR/LF is refused right after argument parsing (exit 2).
    • sanitize flattens CRLF, CR and LF to ; and still replaces —.
  • lane-start
    • The deferred first-act command appends $(quoted --agent "$AGENT") where AGENT_EXPLICIT=1: an agent named on the command line, or a non-default one the record named.
    • It goes after the lane's own arguments, so --no-launch <repo> <n> stays contiguous.
    • I checked that this cannot change how the deferred run binds. For --agent claude, the deferred --no-launch in the live window still takes the resume-by-id branch for that window's own transcript, which comes before the AGENT_EXPLICIT new-session branch.

No doc quotes any message or flag I changed (checked docs/README-lanes.md, skills/*/SKILL.md, commands/*.md).

Functions and regions edited:

  • lanes-edit.sh: append_text_line (new guard at the top of the function, ≈L1679-1696)
  • lanes-edit.sh: main dispatcher, append-line) arm (new guard after the usage check, ≈L13561-13571)
  • lane-handoff: top level, right after the argument-parsing loop (new block ≈L296-313; the why accumulation lines inside the loop are untouched)
  • lane-handoff: sanitize (and its comment, ≈L2168-2186)
  • lane-start: section 5b, the deferred-record note before if (( ! lane_sid_known )) (≈L3894-3910)
  • tests/test_lane_helpers.sh: Amendment 17 section, after the repoHF-7 writer-validation block (≈L9474-9535)
  • tests/test_lane_helpers.sh: section 4, lane-start --agent cases, after repoAG-1 and after repoAG-4 (≈L9960-9968, ≈L10027-10039)

Tests added, and fail-before / pass-after

In tests/test_lane_helpers.sh:

  • Amendment 17 section, repoHF-7:
    • append-line with a newline is refused with exit 2. The register is byte-identical and git status --porcelain is unchanged.
    • The same for a carriage return.
    • append_text_line, lifted out of the helper the way the cut_to_line cases are, refuses a newline itself and leaves its file at its one line.
    • lane-handoff --lane repoHF-7 "$(printf 'clear\nsecond line')" is refused with exit 2. The handoff, the object log and the register are byte-identical, no commit is made, and the checkout is as it was.
    • The same refusal for a trailing CR.
    • sanitize, lifted into its own shell, turns a\r\nb\rc\nd — e into a; b; c; d; e.
  • Section 4, --agent:
    • The codex launch of repoAG-1 defers a first act carrying --agent codex.
    • repoAG-4 (record agent claude, run --agent codex) defers exactly lane-start --no-launch repoAG 4 --agent codex.
    • A default-agent start with no uuid (repoAG-10, LANE_START_SESSION_ID=0) defers --no-launch repoAG 10 with no --agent.

Proof. I did not run the full suite locally (box load, and the AGENTS.md ruling of 2026-09-16). I ran one trimmed copy of the suite: setup lines 1-971, E/LOGD, write_record_a12, and the whole Amendment 17 section through section 4. I ran that same file against two exported trees, git archive f87fd5a and this branch's files, with every LANES_* seam unset and TMPDIR in a scratch directory:

pre  (f87fd5a): 180 passed, 18 failed   — 17 of the new assertions fail (rc 5/0 where 2 is expected, the register/handoff changed, HEAD moved, sanitize kept the breaks, the deferred line without --agent)
post (branch) : 197 passed, 1 failed

The one failure in both runs is …and a record written before Amendment 17 carries both EMPTY, which reads repoSW-1, a fixture from a section the trim leaves out. The new assertions that pass in both trees are expected to:

  • …the object log not written: write_event already refused.
  • …and the checkout as it was: before the fix the damage was committed, not left dirty.
  • The repoAG-10 negative.

Also run locally:

  • bash -n on all four files.
  • python3 -m pytest tests/test_repo_hygiene.py (152 passed).
  • CI's tests job is the suite of record for everything else.

Remaining, not in this PR

Part of #85
Part of #72

🤖 Generated with Claude Code

Summary by Sourcery

Prevent multiline lane records from being written and retain the intended agent when deferring lane-start records.

Bug Fixes:

  • Reject carriage returns and newlines before writing one-line records, preventing partial lane-handoff updates and unintended register changes.
  • Preserve an explicitly selected or recorded agent in deferred lane-start commands.

Enhancements:

  • Flatten line breaks in lane-handoff sanitization as a secondary safeguard.

Tests:

  • Add coverage for one-line writer refusals, atomic lane-handoff behavior, sanitization, and deferred agent preservation.

…s why refuse it before writing, and the deferred lane-start command keeps --agent (#85, #72 items 5, 6)

`append_text_line` appended its text and only then proved the file had grown
by one line, and the `append-line` verb had no guard of its own. A
`lane-handoff` why carrying a newline therefore refreshed and committed a
handoff whose top block was split in two, had its object-log PAUSED refused by
`write_event`, appended two lines to LANES.md, and exited 0 — and the row flip
right after it committed those two lines as a peer's `pre-existing` edit.
Measured in the suite's own sandbox at f87fd5a. A carriage return passed every
proof, because `wc -l` does not count one.

One policy across #85 and #72 item 6: the writers refuse before they write
(`append_text_line` before its mktemp, so every caller has the gate, and the
`append-line` verb before the lock, so `handle_preexisting` cannot commit
anything on behalf of a refusal); a why carrying CR/LF is refused at the door,
before the identity triple, the refresh and every write, which is the only
point that covers all four places the why reaches; and `sanitize` spells any
break `; ` as the second line of defence.

#72 item 5: the command `lane-start` prints for the deferred first act dropped
`--agent`, so a lane resumed `--agent codex` over a record naming claude was
handed a first act that `lane-agent` would settle as claude. It now carries the
agent wherever AGENT_EXPLICIT is 1, quoted, after the lane's own arguments.

Part of #85. Part of #72.

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

The PR closes newline and carriage-return paths that could write malformed one-line records or leave lane-handoff partially committed, by validating inputs before any write or lock and retaining defensive sanitization; it also fixes deferred lane-start commands to preserve an explicitly selected agent, with focused regression coverage.

Sequence diagram for pre-write newline rejection

sequenceDiagram
    participant Caller
    participant LaneHandoff as lane-handoff
    participant AppendLine as append-line
    participant Writer as append_text_line
    participant Register as Register

    Caller->>LaneHandoff: lane-handoff --lane lane why
    alt why contains CR or LF
        LaneHandoff-->>Caller: exit 2
        Note over LaneHandoff,Register: No refresh, lock, write, or commit
    else append-line text contains CR or LF
        Caller->>AppendLine: append-line text
        AppendLine-->>Caller: exit 2
        Note over AppendLine,Register: Lock is not taken
    else one-line text
        AppendLine->>Writer: append_text_line text
        Writer->>Register: append one line
    end
Loading

Sequence diagram for deferred lane-start agent preservation

sequenceDiagram
    participant User
    participant LaneStart as lane-start
    participant Deferred as Deferred lane-start

    User->>LaneStart: lane-start --agent codex lane
    alt transcript UUID unknown
        LaneStart->>Deferred: lane-start --no-launch lane --agent codex
        Deferred-->>User: First act preserves explicit agent
    else default agent with no explicit selection
        LaneStart->>Deferred: lane-start --no-launch lane
        Deferred-->>User: First act uses recorded/default agent
    end
Loading

Flow diagram for safe one-line record handling

flowchart LR
    Input[Input text] --> Validate{Contains CR or LF?}
    Validate -- Yes --> Refuse[Exit 2; nothing written]
    Validate -- No --> Sanitize[sanitize replaces line breaks with ; ]
    Sanitize --> Write[Write one-line record]
Loading

File-Level Changes

Change Details Files
Prevent embedded line breaks from corrupting one-line records or partially completing a lane handoff.
  • Reject CR/LF in the shared append helper before temporary-file creation or writing.
  • Reject CR/LF in the append-line command before lock acquisition.
  • Reject invalid lane-handoff why text immediately after argument parsing, before refreshes or commits.
  • Retain sanitize as a defensive fallback that converts CR, LF, and CRLF to ; .
lanes-edit.sh
lane-handoff
tests/test_lane_helpers.sh
Preserve an explicitly selected agent in deferred lane-start commands.
  • Append a quoted --agent argument when the launch agent is explicit or differs from the default.
  • Keep the lane arguments contiguous and avoid adding the flag when no explicit agent needs to be preserved.
  • Add coverage for codex launches, record-agent mismatches, and default-agent starts.
lane-start
tests/test_lane_helpers.sh
Add regression tests proving refusal is atomic and deferred commands retain agent selection.
  • Verify newline and carriage-return inputs leave handoff, log, register, git status, and HEAD unchanged.
  • Verify the shared append helper independently rejects multiline input.
  • Verify sanitize flattening and exact deferred command output.
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

…2, so CI 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>
…2, so CI 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
brettheap marked this pull request as ready for review October 18, 2026 15:58
@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 01eff2c into main Oct 18, 2026
5 checks passed
brettheap added a commit that referenced this pull request Oct 18, 2026
…ane-handoff renames the window only once the handoff can no longer be refused (#80 slices B and C) (#220)

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 39137756635) was green on head 0b531e5 with main 586dea2 in its tree. Main 01eff2c merged with head 0b531e5 is tree a388312, which queue run 39137998354 (workflow_dispatch of tests on f335793) tested whole and green.

Independent review (lane 5, Opus, of 9f6fd2d; the merges of main add nothing to the PR's diff): LAND. Brett Heap confirmed the rename trade as built (2026-10-18).

Part of #80 (slices B and C).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
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>
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