diff --git a/lane-handoff b/lane-handoff index e854cb7a..cb5f3730 100755 --- a/lane-handoff +++ b/lane-handoff @@ -293,6 +293,24 @@ while [ $# -gt 0 ]; do esac done +# THE WHY IS ONE LINE, AND IT IS REFUSED AT THE DOOR (opensoft/openRepoTools#72 +# item 6, #85). It reaches FOUR places, and only the last two pass through +# `sanitize`: the handoff's top block, where a newline writes a malformed prompt +# (a `---` line among them, which is where `lane-start`'s `handoff_top_block` +# ends the first prompt it hands the next session); the handoff commit's +# subject; the object log's PAUSED line, where `write_event` refuses it, but +# only AFTER section 2 has refreshed and committed the handoff, so the act ends +# with a top block and no record behind it; and the register's file-level +# line, where `append-line` used to write it first and refuse it after. One +# test here covers all four, and it comes before the identity triple, the +# refresh and every write, so the refusal leaves nothing to undo. It is the +# rule `lane-end --reason` and `rename-lane`'s reason already keep for a free +# text that becomes one line. `sanitize` flattens a newline as well, but that +# is the second line of defence and not this one. +if [ "${why//[$'\n\r']/}" != "$why" ]; then + die "the why is ONE line: it is written into the handoff's top block, its commit message, the object log's PAUSED line and the register's file-level line, and a newline or carriage return in it would split each of them — the last two in files nothing rewrites. Nothing was written. Put it on one line, with '; ' where the break was, and re-run." 2 +fi + # ONE ACT AT A TIME AFTER THE RECORD. `--restart` respawns the pane for a lane # that STAYS HERE and `--exit` ends the session for a lane going somewhere else # (Amendment 18(d)); asking for both is asking the same pane to do two opposite @@ -2150,7 +2168,22 @@ fi # THE FREE TEXT IS SANITIZED ONCE: Amendment 7(b)/R26 refuses any payload or # free text containing ` — ` (exit 2), because that separator is what divides a # line's fields from its free text. -sanitize() { printf '%s' "$1" | sed 's/ — /; /g'; } +# +# AND A LINE BREAK IS SPELLED `; ` TOO (opensoft/openRepoTools#85), whatever +# its spelling — CRLF, a lone CR, a lone LF — so that one break is one `; `. +# Both writers this text reaches refuse a newline before they write +# (`write_event` for the log, `append-line` for the register), and a why that +# carries one is refused at the door, right after the arguments are read, so +# nothing that reaches here today carries one. This is the second line of +# defence, in the shape #85 asks of the caller: where a break does arrive, the +# act still happens, on one line. `sed` cannot do it, because `sed` reads a +# line at a time and hands the break back. +sanitize() { + sz="${1//$'\r\n'/; }" + sz="${sz//$'\r'/; }" + sz="${sz//$'\n'/; }" + printf '%s' "$sz" | sed 's/ — /; /g' +} # THE FREE TEXT NAMES THE KIND AFTER ITS WHY (Amendment 17 Addendum 1 (h), in # force 2026-09-14T20:59:31Z): `clear in-process`, `clear respawn`, `switch # respawn`, `reset respawn` — the why the caller gave, then the word for what diff --git a/lane-start b/lane-start index 810a5601..d00e3195 100755 --- a/lane-start +++ b/lane-start @@ -4032,9 +4032,23 @@ fi # writes this line and the handoff's stamp, both carrying the real uuid. # (`lane_sid_known` is computed with the launch decision above, which is where # `lane_sid` is decided and where the row's own stamp first reads it.) +# +# AND THE COMMAND IT PRINTS CARRIES THE AGENT THIS RUN WAS GIVEN +# (opensoft/openRepoTools#72 item 5). `$self_args` is the lane and nothing +# else, so a lane started `--agent codex` — which never has a uuid here, so it +# ALWAYS reaches this line — was handed a first act that dropped the flag, and +# the person who ran it had `lane-agent` choose instead: the agent of the lane's +# last PAUSED, which is whichever agent paused it and not the one now running +# it. `AGENT_EXPLICIT` is 1 for an agent named on the command line and for one +# the record named that is not the default; either way the run that settles +# this one is the same agent's, so it is written down, quoted like every other +# command this file prints. It goes AFTER the lane's own arguments, so the +# `--no-launch ` a reader looks for is still one run of words. +ls_defer_agent="" +if (( AGENT_EXPLICIT )); then ls_defer_agent=" $(quoted --agent "$AGENT")"; fi if (( ! lane_sid_known )); then note "no transcript uuid is knowable at this instant (the row's stamp says '${lane_sid:-unknown}'), so the lane's $lane_verb log line is NOT written: Amendment 7(b) makes that field a transcript uuid and only that, and the log is append-only. Nor is the handoff's Rule 3 stamp. BOTH are written by the first act that knows the uuid, which is this session's own:" - note " $prog --no-launch $self_args" + note " $prog --no-launch $self_args$ls_defer_agent" elif (( dry_run )); then plan "lanes-edit.sh log $lane_verb lane:$LANE → \"$lane_payload\"" else diff --git a/lanes-edit.sh b/lanes-edit.sh index b7232eca..de48539c 100755 --- a/lanes-edit.sh +++ b/lanes-edit.sh @@ -1692,6 +1692,25 @@ delete_lines() { # # more, not one existing byte different. append_text_line() { newline="$1"; target="${2:-$LANES_FILE}" + # ONE LINE IS REFUSED A NEWLINE BEFORE IT IS WRITTEN, NOT PROVED ONE AFTER + # (opensoft/openRepoTools#85). The proofs below are made AFTER the `>>`, so a + # text carrying a newline was appended whole — two physical lines where one + # was meant — and only then refused: `append changed line count by 2`, exit + # 5, the file left modified and uncommitted. What came next was worse than + # the refusal. A dirty object log is refused by every later writer in that + # checkout; a dirty REGISTER is committed by the next one, as a peer's + # `pre-existing` edit (`handle_preexisting`) — measured in `lane-handoff`'s own + # flow, where the row flip right after the refused PAUSED line committed the + # two lines. A carriage return is refused with it: `wc -l` does not count one, so it + # passed the proof and put a line no reader splits the same way into a file + # nothing rewrites. `write_event` has refused both since R21; this is the + # same gate in the one function every append goes through, which is the + # reason this file gives for putting the container guard at the dispatcher: + # *"nine copies of one rule is how eight of them would come to disagree"*. + # FIRST, before the `mktemp` and the copy, so a refusal touches nothing. + if [ "${newline//[$'\n\r']/}" != "$newline" ]; then + die "an appended line is ONE line, and this text carries a newline or a carriage return: appended, it would put several lines into $target where one was meant, and the proof that only one was added fails after the write, not before it. Nothing was written. Spell the break '; ' and re-run." 2 + fi TMPD="$(mktemp -d)" pre="$TMPD/pre" cat -- "$target" > "$pre" @@ -13610,6 +13629,17 @@ Nothing was written." 2 append-line) text="${1-}"; [ -n "$text" ] || die "usage: append-line \"\"" 2 + # ONE LINE, REFUSED A NEWLINE BEFORE THE LOCK (opensoft/openRepoTools#85). + # `append_text_line` refuses it too, and before it writes — but that is + # after `acquire_lock` and `handle_preexisting`, which can COMMIT a peer's + # uncommitted register edit before this act's own write. A refusal that has + # already committed something is a refusal that wrote, so the verb asks + # first, exactly where `write_event` asks for `log`: the checkout is left + # as this command found it. `lane-handoff`'s file-level PAUSED line is the + # caller that met it, with a `why` carrying a newline. + if [ "${text//[$'\n\r']/}" != "$text" ]; then + die "append-line appends ONE line to the register, and this text carries a newline or a carriage return: it would append several, and the proof that only one was added would fail after the write, leaving the register modified and uncommitted. Nothing was written, and the lock was not taken. Spell the break '; ' and re-run." 2 + fi # NO MARKER BY THE BACK DOOR (Copilot round 3 on #97): `add-row` refuses a # row in the marker's vocabulary, and this appends any line at all to the # register — a row-shaped one included. diff --git a/tests/test_lane_helpers.sh b/tests/test_lane_helpers.sh index b06a7cdd..5945ef50 100755 --- a/tests/test_lane_helpers.sh +++ b/tests/test_lane_helpers.sh @@ -9566,6 +9566,69 @@ is "…and nothing was written by any of the three" "$(grep -c '^PAUSED' "$LO # `PAUSED` lines on purpose: section 3 below is the `--late` rule, and its whole # question is what a lane with no paused record does.) +# A ONE-LINE RECORD CANNOT TAKE A NEWLINE, AND IT IS REFUSED BEFORE ANYTHING IS +# WRITTEN (opensoft/openRepoTools#85, #72 item 6). `append-line` used to append +# its text and only THEN prove it was one line, so a text carrying a newline +# left two lines in the register, the checkout dirty, and a refusal (exit 5) +# that came after the write. And `lane-handoff` put its `why` into four places +# with only two of them sanitized: a `why` carrying a newline refreshed and +# committed the handoff, had its PAUSED refused by `write_event`, handed +# `append-line` two lines — which the row flip after it then committed as a +# peer's `pre-existing` edit — and exited 0. Every case here is asked on +# repoHF-7, whose log is kept EMPTY of `PAUSED` lines for section 3: a refusal +# writes nothing, and that is exactly what these assert. +a17_nl_head="$(git -C "$WIP" rev-parse HEAD)" +a17_nl_status="$(git -C "$WIP" status --porcelain)" +cp -- "$LANES" "$SANDBOX/a17-nl-lanes.md" +cp -- "$WIP/handoffs/repoHF/repoHF-7.md" "$SANDBOX/a17-nl-handoff.md" +cp -- "$LOGD/repoHF-7.md" "$SANDBOX/a17-nl-log.md" +a17_nl_same() { # — `same` where not one byte moved + if cmp -s -- "$1" "$2"; then printf same; else printf changed; fi +} +run env LANES_LANE=repoHF-7 "$E" append-line \ + "$(printf 'PAUSED — lane repoHF-7, session %s@Eagle, 2026-09-14T09:30:00Z, clear\nsecond line; handoff refreshed' "$HF4_ID")" +is "append-line refuses a text carrying a newline" "$rc" 2 +has "…saying it appends ONE line" "$err" "append-line appends ONE line" +is "…before it writes: the register is byte for byte as it was" \ + "$(a17_nl_same "$LANES" "$SANDBOX/a17-nl-lanes.md")" same +is "…and the checkout is as it was, so the next writer in it is not refused for a dirty register" \ + "$(git -C "$WIP" status --porcelain)" "$a17_nl_status" +run env LANES_LANE=repoHF-7 "$E" append-line "$(printf 'LANDING — lane repoHF-7, a carriage\rreturn')" +is "…and a carriage return the same way, which the line count after the write never saw" "$rc" 2 +is "…with nothing written for it either" "$(a17_nl_same "$LANES" "$SANDBOX/a17-nl-lanes.md")" same + +# AND THE GATE IS IN `append_text_line` ITSELF, so every caller has it — `add-row`, +# `log`'s Rule 6 line, a rename's lines — and not only the verb that met it. +# Lifted out of the helper the way the `cut_to_line` cases below lift theirs, +# and asked against a file of its own. +printf 'line one\n' > "$SANDBOX/a17-nl-atl.txt" +run bash -c 'eval "$(sed -n -e "/^die()/,/^}/p" -e "/^note()/p" -e "/^append_text_line()/,/^}/p" "$1")"; append_text_line "$2" "$3"' \ + _ "$E" "$(printf 'two\nthree')" "$SANDBOX/a17-nl-atl.txt" +is "append_text_line refuses a newline itself, whichever verb called it" "$rc" 2 +is "…before its copy and its \`>>\`: the file is still its one line" "$(cat "$SANDBOX/a17-nl-atl.txt")" "line one" + +run env PATH="$A17PATH" CLAUDE_CODE_SESSION_ID="$HF4_ID" CLAUDE_PROFILE_NAME=team-05a \ + "$HANDOFF_CMD" --lane repoHF-7 "$(printf 'clear\nsecond line')" +is "lane-handoff refuses a why carrying a newline" "$rc" 2 +has "…at the door, saying the why is one line" "$err" "the why is ONE line" +is "…the handoff not refreshed" \ + "$(a17_nl_same "$WIP/handoffs/repoHF/repoHF-7.md" "$SANDBOX/a17-nl-handoff.md")" same +is "…the object log not written" "$(a17_nl_same "$LOGD/repoHF-7.md" "$SANDBOX/a17-nl-log.md")" same +is "…the register byte for byte as it was" "$(a17_nl_same "$LANES" "$SANDBOX/a17-nl-lanes.md")" same +is "…nothing committed" "$(git -C "$WIP" rev-parse HEAD)" "$a17_nl_head" +is "…and the checkout as it was" "$(git -C "$WIP" status --porcelain)" "$a17_nl_status" +run env PATH="$A17PATH" CLAUDE_CODE_SESSION_ID="$HF4_ID" CLAUDE_PROFILE_NAME=team-05a \ + "$HANDOFF_CMD" --lane repoHF-7 "$(printf 'clear\r')" +is "…and a carriage return is refused the same way" "$rc" 2 + +# `sanitize` IS THE SECOND LINE OF DEFENCE: a break that reaches it is spelled +# `; `, one per break whatever its spelling, so the act still happens on one +# line. Lifted out of `lane-handoff` in a shell of its own, because this file +# has a `sanitize` of its own (the transcript directory's). +run bash -c 'eval "$(sed -n "/^sanitize()/,/^}/p" "$1")"; sanitize "$2"' \ + _ "$HANDOFF_CMD" "$(printf 'a\r\nb\rc\nd — e')" +is "lane-handoff's sanitize spells every line break '; ', as it does ' — '" "$out" "a; b; c; d; e" + # `--utc` IS THE LATE `PAUSED`'s SEAM AND HAS NO OTHER CALLER (Amendment 17, # adoption act 7). This log is append-only and FILE ORDER is what every state # read means by "last", so a STARTED or a RESUMED dated by hand changes what @@ -9989,6 +10052,15 @@ has "…the stamp naming the agent and its own id" \ has "…the row's session cell appended in the agent's own spelling" \ "$(grep '^| `repoAG-1`' "$LANES")" "→ Codex \`$CODEX_ID\`" has "…and the row's stamp says who resumed it" "$(grep '^| `repoAG-1`' "$LANES")" "RESUMED by codex $CODEX_ID (lane repoAG-1)" +# THE FIRST ACT IT DEFERS KEEPS THE AGENT (opensoft/openRepoTools#72 item 5). +# `codex` keeps no id this command can pass, so a codex launch ALWAYS defers its +# log line and its Rule 3 stamp to the session's own `--no-launch` — and the +# command printed for that act dropped `--agent`, leaving `lane-agent` to choose: +# the agent of the lane's last PAUSED, not the one now running it. +ag_deferred() { # — the one command lane-start prints for the deferred records + printf '%s\n' "$1" | awk 'f { print; exit } /which is this session.s own:$/ { f = 1 }' +} +has "…and the first act it defers to the session carries --agent codex" "$(ag_deferred "$err")" " --agent codex" : > "$FAKE_CODEX_LOG" export FAKE_CODEX_WATCH="$WIP/handoffs/repoAG/repoAG-2.md" @@ -10047,6 +10119,19 @@ has "…saying whose the recorded transcript is, and that this launch does not "$err" "names agent claude and this launch is codex" hasnt "…and the session cell is NOT appended with a Claude uuid in Codex's spelling" \ "$(grep '^| `repoAG-4`' "$LANES")" "Codex \`$HF2_ID\`" +# THE CASE #72 ITEM 5 IS ABOUT: the record names claude and the person named +# codex, so a deferred act without the flag would be settled by claude. +is "…and its deferred first act names codex, which this lane's record would not" \ + "$(ag_deferred "$err")" "lane-start: lane-start --no-launch repoAG 4 --agent codex" +# AND A RUN THAT NAMED NO AGENT, OVER A RECORD THAT NAMES NONE, PRINTS NONE: +# the flag is written down where this run's agent was explicit and only there, +# so the deferred act chooses its agent exactly as this run did. repoAG-10 has +# no row and no log, and with nothing minted its records are deferred too. +run env PATH="$A17PATH" FAKE_TMUX_WINDOW="agsess:@41" CLAUDE_PROFILE_NAME=team-05a LANE_START_SESSION_ID=0 \ + "$START" --dir "$AG_DIR" repoAG-10 --no-launch +is "a default-agent start with no uuid to name exits 0" "$rc" 0 +has "…deferring its records to the session's own first act" "$(ag_deferred "$err")" "--no-launch repoAG 10" +hasnt "…and naming no agent in it" "$(ag_deferred "$err")" "--agent" # A LANE WITH NO ROW IS A `STARTED`, WHATEVER BRANCH LAUNCHED IT. `--agent` and # the `/ctx` seam both set the verb to RESUMED because they resume a lane that