From 068f190c32f35f9816b4cc84b48b76ccb5ece146 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Thu, 22 Oct 2026 15:33:23 +0000 Subject: [PATCH 01/19] lane-start writes the Rule 3 stamp with one `write-handoff --stamp` through the lane's checkout, fast-forwards W before and after its writes, and is refused after its first write only by a restart's own fences (#198, slice 2b) Amendment 23, act 2's slice 2b, its `lane-start` half (lane openRepoTools-3's finding 282 on #307, its first writer): - 5c is one `LANES_LANE= lanes-edit.sh write-handoff --stamp --subject [--restart ... --expected-digest ]`, with no LANES_NO_FETCH. `hs_git_net` and `hs_carry_note` are deleted, and with them the publish under W's lock and the add/commit/pull/push in W outside any lock. Its exit is a note; under a supervised restart a 7, or a bound file no longer at the bound digest, still dies. - `refresh-workspace` runs before the first read and again before section 6, under the opt-in with the default resolution and never in a dry run; every exit is a note. The row is read from W after the first refresh, or from W's origin/main where it did not fast-forward; slice 1a's `lane_register_follow` is gone from lane-start. - The lane's checkout is made at "5. the row" (`lane-checkout --create`, a note on refusal), and the writable handoff path `handoff-path` names is printed beside the row's spelling. - The restart binding and 5d read the file `handoff-path` names; an ordinary launch whose own copy is missing falls back to `hs_resolve`, said. - The stale-checkout recoveries name `refresh-workspace` (or fetch and `merge --ff-only` for a caller's checkout) and the publish act, never a pull or a bare push. `request-handoff` gains LANES_LANE. Tests: new tests/test_a23_lane_start.py (eight cases, red at e2037b9); tests/test_a23_lane_start_read.py's lane-start cases follow the new read; the shell suite's RV-B2/RV-B3 (CN-1..3) and the :3189 recovery text; hygiene's no-pull rule covers lane-start. Refs #198 Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 (cherry picked from commit f1b19e96637eb3f67f76d645ac6c8fb5b40869a6) --- lane-start | 535 ++++++++++++++---------------- tests/test_a23_lane_start.py | 301 +++++++++++++++++ tests/test_a23_lane_start_read.py | 37 ++- tests/test_lane_helpers.sh | 117 +++---- tests/test_repo_hygiene.py | 7 +- 5 files changed, 622 insertions(+), 375 deletions(-) create mode 100644 tests/test_a23_lane_start.py diff --git a/lane-start b/lane-start index 3723114..386bea2 100755 --- a/lane-start +++ b/lane-start @@ -43,9 +43,13 @@ # as the line after its header block: # RESUMED by (lane ) at — lane-start on # committed on its own as `handoff(@): RESUMED by `, with -# an explicit pathspec and a `Lane:` trailer (Amendment 4(d)), then pulled -# and pushed. It NEVER refuses the launch: a stamp that cannot be written -# prints the command that writes it. Seven resumes of one lane needed that +# an explicit pathspec and a `Lane:` trailer (Amendment 4(d)), and pushed: +# ONE `lanes-edit.sh write-handoff --stamp`, through the checkout every +# write of the lane goes through and under its lock (Amendment 23, slice +# 2b). It NEVER refuses the launch: a stamp that cannot be written prints +# the line and the command that writes it, and once the first register +# write is made only a supervised restart's own fences stop a launch. +# Seven resumes of one lane needed that # line written by hand, and while it was owed the counts drifted 6/4/5 # across the row, the object log and the handoff itself. # @@ -322,12 +326,20 @@ # the answer; anything but y/yes is No # LANES_REPO the checkout the register and the handoffs share # (default: the workspace repository) +# LANES_ROUTE_WRITES=1 Amendment 23's opt-in: with neither LANES_REPO nor +# LANES_FILE set, every write of the lane goes through +# its own register checkout -lanes//, made at +# "5. the row", and W, the workspace repository, is +# fast-forwarded (`lanes-edit.sh refresh-workspace`) +# before the row is read and again before the launch # LANES_WORKSPACE_ROOT the workspace repository, overriding the `path:` in # ${AGENT_PROTOCOL_ROOT:-~/.agents}/workspace.yaml — the # test seam, and the only override there is -# LANES_BRANCH default main — the branch the handoff stamp is pushed on -# LANES_GIT_TIMEOUT seconds bounding each git call that touches the NETWORK -# (default 60; 0 or a non-number turns the bound off) +# LANES_BRANCH default main — the register's branch: what every write +# pushes, and what W is fast-forwarded to +# LANES_GIT_TIMEOUT seconds bounding each git call `lanes-edit.sh` makes +# over the NETWORK (default 60; 0 or a non-number turns +# the bound off). This command makes none of its own # LANES_GIT the git binary THOSE calls use, so a test can hand them # a git that hangs without shadowing git for the script # LANES_NO_GIT=1 write the handoff's stamp but commit nothing @@ -1106,6 +1118,36 @@ if [ -z "${LANES_FILE:-}" ]; then die "$(lanes_workspace_why)" 1; fi [ -f "$LANES_FILE" ] || die "no register at $LANES_FILE — the workspace repository $(lanes_ws_yaml) names carries no lanes/LANES.md. \`openRepoTools wip init\` seeds one (it is idempotent); or set LANES_FILE" 1 [ -x "$LANES_EDIT" ] || die "no register writer at $LANES_EDIT — \`lanes-edit.sh\` is installed beside this command by \`openRepoTools --install\`. Run that, or set LANES_EDIT" 1 +# AMENDMENT 23 S5 (slice 2b) — W IS FAST-FORWARDED BEFORE IT IS READ, AND AGAIN +# ONCE THIS LAUNCH'S WRITES ARE DONE. Under `LANES_ROUTE_WRITES=1` and the +# default resolution every write of this lane goes through its own register +# checkout, `-lanes//`, and W, the readers' copy this command reads +# the lane's row from, moves only by `lanes-edit.sh refresh-workspace` (plan +# §2.6): its fetch, then `merge --ff-only`, and never a reset, a stash, a commit +# or an abort. So it is asked for twice: here, before the first read through W, +# and just before section 6, so that the next reader of W finds this launch's +# own row, log line and stamp. Without the opt-in W is the write checkout, and +# every write's S3 fast-forwards it under its own lock; an explicit +# `LANES_REPO` or `LANES_FILE` is the caller's register and not W. Neither +# refresh runs then, nor in a dry run, which plans it. Every exit is a note: a +# launch is never refused for a read it can do without (Amendment 8(d)). +LS_ROUTED=0 +if [ "${LANES_ROUTE_WRITES:-}" = 1 ] && [ "$LANES_REG_DEFAULT" = 1 ]; then LS_ROUTED=1; fi +LS_REFRESH_RC="" +ls_refresh() { # + [ "$LS_ROUTED" = 1 ] || return 0 + if (( dry_run )); then + plan "$LANES_EDIT refresh-workspace # W, fast-forwarded $1 (Amendment 23 S5)" + return 0 + fi + LS_REFRESH_RC=0 + "$LANES_EDIT" refresh-workspace || LS_REFRESH_RC=$? + [ "$LS_REFRESH_RC" = 0 ] || + note "W was not fast-forwarded $1 (\`$LANES_EDIT refresh-workspace\` exited $LS_REFRESH_RC; its words are above), and the launch goes on: W is the readers' copy, and only a person repairs it (Amendment 23 S5)." + return 0 +} +ls_refresh "before this launch reads it" + # ------------------------- AMENDMENT 15: THE ROW'S SPELLING IS THE LANE'S NAME # # The row wins over the directory, and over what was typed: it is what the @@ -1164,51 +1206,38 @@ esac repo="${repo:-$LANE}" # the verbatim form has no of its own estate="${estate:-$repo}" -# AMENDMENT 23 S1, S2 — THE REGISTER THIS COMMAND READS IS THE ONE ITS WRITES GO -# THROUGH. Under `LANES_ROUTE_WRITES=1` (opt-in until slice 1b, the review of -# #282's MUST 2) and the default resolution, every write of this lane goes -# through its own checkout, `-lanes//`, and W, the readers' copy, moves -# only by `lanes-edit.sh refresh-workspace`. So W's working tree does not carry the -# lane's own rows once they are written: a lane started through its checkout -# read as a lane with no row, and a second start refused to add a duplicate -# that W could not see. The row this command counts and acts on is therefore -# read from that checkout. The helper makes the checkout here if it is not -# there yet, exactly as this command's first write would; a dry run makes -# nothing and reads W where there is no checkout. ONLY THE READ MOVES: the -# handoff this command publishes is still W's until Amendment 23's later slices. -# A helper that predates the verb writes W itself, so W is what is read. A -# checkout that could not be made (2) or whose creation lock is held (4) is -# SAID, and W is read (SHOULD 2): Amendment 8(d) — a launch is never refused for -# a register act it can do without, and the writes after it say their own. -lane_register_follow() { - [ "${LANES_ROUTE_WRITES:-}" = 1 ] || return 0 - [ "$LANES_REG_DEFAULT" = 1 ] && [ -n "${LANE:-}" ] || return 0 - case "$prog" in lane-end) lrf_none="Nothing was written." ;; *) lrf_none="Nothing was launched, renamed or written." ;; esac - lrf_args=(lane-checkout "$LANE") - [ "${dry_run:-0}" = 1 ] || lrf_args+=(--create) - lrf_errf="$(mktemp "${TMPDIR:-/tmp}/${prog}-register.XXXXXX" 2>/dev/null || printf '')" - lrf_out=""; lrf_rc=0 - lrf_out="$("$LANES_EDIT" "${lrf_args[@]}" 2>"${lrf_errf:-/dev/null}")" || lrf_rc=$? - lrf_err="" - if [ -n "$lrf_errf" ]; then lrf_err="$(cat -- "$lrf_errf" 2>/dev/null || :)"; rm -f -- "$lrf_errf"; fi - case "$lrf_rc" in - 0) - [ -z "$lrf_err" ] || printf '%s\n' "$lrf_err" >&2 - [ -f "$lrf_out/lanes/LANES.md" ] || - die "lane $LANE's register checkout $lrf_out carries no lanes/LANES.md, so the lane's row cannot be read (Amendment 23 S1). $lrf_none" 1 - LANES_FILE="$lrf_out/lanes/LANES.md" ;; - 8) : ;; # a dry run, and no checkout yet: W is what there is - 2 | 4) - case "$lrf_err" in *"unknown subcommand 'lane-checkout'"*) return 0 ;; esac - [ -z "$lrf_err" ] || printf '%s\n' "$lrf_err" >&2 - note "lane $LANE's own register checkout could not be made (\`$LANES_EDIT lane-checkout $LANE --create\` exited $lrf_rc, above), so this command reads W's register, which may not show what this lane's earlier writes put there (Amendment 23 S1, 8(d)). Each write below says for itself whether it went through." ;; - *) - [ -z "$lrf_err" ] || printf '%s\n' "$lrf_err" >&2 - die "lane $LANE's own register checkout could not be read (\`$LANES_EDIT lane-checkout $LANE\` exited $lrf_rc, above), so the row this command acts on cannot be read where its writes go (Amendment 23 S1). $lrf_none" 1 ;; - esac +# AMENDMENT 23 S1, S5 (slice 2b) — THE ROW IS READ FROM W, BROUGHT UP TO DATE +# FIRST. Under the opt-in and the default resolution every write of this lane +# goes through its own checkout, and W, which takes none, is fast-forwarded +# above, so W's working tree carries every row that has landed, this lane's +# own included. (Slice 1a read the row from the lane's checkout instead, and +# made that checkout here to do it; it is made at "5. the row" now, where the +# first write needs it.) Where that refresh did not fast-forward W, or in a dry +# run, which runs none, the row is read from `origin/` as W last +# fetched it, never from a working tree that may be behind; and where W has no +# such ref, from the working tree, said. Without the opt-in, or with a +# caller's `LANES_REPO` or `LANES_FILE`, the register read is `$LANES_FILE`'s +# working tree, as it always was. Held as text, not a temporary file, because +# the `exec` this command ends in runs no exit trap. +LS_ROWS_FROM=file; LS_ROWS_TEXT="" +ls_rows_source() { + [ "$LS_ROUTED" = 1 ] || return 0 + [ "$LS_REFRESH_RC" != 0 ] || return 0 + lrs_ref="refs/remotes/origin/${LANES_BRANCH:-main}" + if LS_ROWS_TEXT="$(git -C "$LANES_REPO" show "$lrs_ref:lanes/LANES.md" 2>/dev/null)"; then + LS_ROWS_FROM=ref + (( dry_run )) || note "so this lane's row is read from origin/${LANES_BRANCH:-main} as W last fetched it, not from W's working tree" + else + LS_ROWS_TEXT="" + note "W has no origin/${LANES_BRANCH:-main} to read the register from, so this lane's row is read from W's working tree, which may be behind origin (Amendment 23 S5)" + fi return 0 } -lane_register_follow +ls_rows_source +# The register text the row is counted in: the one source above, for both reads. +ls_register_text() { + if [ "$LS_ROWS_FROM" = ref ]; then printf '%s\n' "$LS_ROWS_TEXT"; else cat -- "$LANES_FILE"; fi +} # ----------------------------------------- 1b. THE LANE'S DIRECTORY # @@ -1621,24 +1650,24 @@ seam_refuse "$LANE" "a lane start" # `openXfactory-2` at 2026-09-13T23:51:56Z. Case-insensitive, the lane is found, # the branch below is the RESUME branch, and `add-row` is never reached. rows_for_lane() { - awk -v lane="$1" ' + ls_register_text | awk -v lane="$1" ' substr($0,1,1) == "|" { p1 = index($0, "`"); if (p1 == 0) next rest = substr($0, p1 + 1); p2 = index(rest, "`"); if (p2 == 0) next if (tolower(substr(rest, 1, p2 - 1)) == tolower(lane)) print - }' "$LANES_FILE" + }' } # The names those rows carry — which under Amendment 15 may be two spellings of # ONE name, and a refusal that could not say so would send a person looking for # a duplicate that is not there to find. row_names_for_lane() { - awk -v lane="$1" ' + ls_register_text | awk -v lane="$1" ' substr($0,1,1) == "|" { p1 = index($0, "`"); if (p1 == 0) next rest = substr($0, p1 + 1); p2 = index(rest, "`"); if (p2 == 0) next t = substr(rest, 1, p2 - 1) if (tolower(t) == tolower(lane)) print t - }' "$LANES_FILE" + }' } row="$(rows_for_lane "$LANE" || :)" @@ -1689,7 +1718,15 @@ if [ "$row_count" = 0 ]; then printf ' %s\n' "${pub_row_first:0:200}" >&2 note "adding a row here would make two, and two rows for one lane is a register lanes-edit.sh cannot edit." note "RECOVERY — bring this checkout up to date, then re-run this command:" - note " git -C \"$(cd -- "$(dirname -- "$LANES_FILE")/.." && pwd)\" pull --rebase origin main" + # NEVER A PULL (Amendment 23 S3, S5): W, the default register read, is + # only ever fast-forwarded, and by one act; a caller's own checkout is + # brought up the same two ways. + if [ "$LANES_REG_DEFAULT" = 1 ]; then + note " $LANES_EDIT refresh-workspace # W is only ever fast-forwarded; it says what blocks it" + else + ls_top="$(cd -- "$(dirname -- "$LANES_FILE")/.." && pwd)" + note " git -C \"$ls_top\" fetch origin ${LANES_BRANCH:-main} && git -C \"$ls_top\" merge --ff-only origin/${LANES_BRANCH:-main}" + fi die "refusing to add a second row for $LANE" 2 ;; *) note "the helper's own output is above." @@ -1710,7 +1747,15 @@ else note "this checkout has a row for $LANE and the register on origin/main has none — so there is no PUBLISHED session cell, and no recorded session to resume." unpub_ids="$(printf '%s\n' "$row" | awk -F'|' '{print $3}' | grep -oiE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | tr 'A-F' 'a-f' | tr '\n' ' ' || :)" [ -n "$unpub_ids" ] && note " the id(s) only this checkout's copy carries, which nothing else can see: ${unpub_ids% }" - note " RECOVERY — push the register from here, then re-run: git -C \"$(cd -- "$(dirname -- "$LANES_FILE")/.." && pwd)\" push origin main" ;; + # THE PUBLISH ACT, NEVER A BARE PUSH (Amendment 23 S3): `commit` with + # nothing staged publishes what the checkout has that origin lacks, and + # carries a hand edit of the register as its own commit. W takes no + # write at all, so a row only W carries is a stray there (S5). + if [ "$LS_ROUTED" = 1 ]; then + note " RECOVERY — that row is a stray in W, the readers' copy, where no tool writes (Amendment 23 S5; \`$LANES_EDIT refresh-workspace\` names it): make it a write of the lane's own checkout and publish it from there, LANES_LANE=$LANE $LANES_EDIT commit \"\", or take it out of W, then re-run." + else + note " RECOVERY — publish the register from here, then re-run: LANES_LANE=$LANE $LANES_EDIT commit \"\" (the publish act, in $(cd -- "$(dirname -- "$LANES_FILE")/.." && pwd))" + fi ;; *) note "the helper's own output is above." die "$LANES_EDIT register-row $LANE failed (exit $pub_rc). That is NOT 'the register has no row for $LANE', and this script will not pick a resume target on an answer it did not get: resuming the working tree's id lands the operator in the lane's previous conversation and stamps that uuid on an append-only line. Fix the helper (or run it by hand: $LANES_EDIT register-row $LANE), then re-run." 1 ;; @@ -2557,7 +2602,9 @@ if [ "$bind_rc" = 0 ]; then (( dry_run )) && ls_rh+=(--dry-run) step "$(quoted "${ls_rh[@]}")" ls_rh_rc=0 - "${ls_rh[@]}" || ls_rh_rc=$? + # ITS LANE, NAMED (Amendment 23 S2): `request-handoff` writes the lane's + # record, so the lane its write belongs to is said rather than derived. + LANES_LANE="$LANE" "${ls_rh[@]}" || ls_rh_rc=$? case "$ls_rh_rc" in 0) bind_recheck=1; step "lane $LANE is free — binding it here (Amendment 18(c))" ;; 8) bind_recheck=1; step "lane $LANE was already free by the time the request was made — binding it here" ;; @@ -3191,6 +3238,11 @@ hs_workspace_root() { lanes_workspace_root; } # somewhere else. # A cell that spells a path of its own is answered by (a) alone: joining an # absolute path on to a root gives a path nothing has. +# SINCE AMENDMENT 23's SLICE 2B IT IS A FALLBACK AND NEVER A TARGET: the stamp +# is written, the restart's binding is checked and the first prompt is read in +# the file `lanes-edit.sh handoff-path` names, the one this lane's writes go +# to. Only an ordinary launch whose own copy is not there reads the copy this +# finds for its first prompt, and says so (5d). hs_resolve() { local cell="$1" cand root [ -n "$cell" ] || return 1 @@ -3346,9 +3398,13 @@ case "$ri_state" in die "lane $LANE's handoff has changed since its restart intent was written: the intent records digest $ri_dig for $ri_hf and that file now digests $ri_now. The top block of that file is this launch's FIRST PROMPT, so launching now would hand the new session the block of a handoff the /ctx did not write. Nothing was renamed, written or started (operation ${ri_op:-none}). Take the handoff again: lane-handoff --restart" 2 fi fi - # The digest and the actual prompt must refer to the same row-bound file. - ri_cell="$(cell 7 | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')" - ri_row_hf="$(hs_resolve "$ri_cell")" || die "the restart handoff the row names is missing. Nothing was started." 2 + # The digest and the actual prompt must refer to the same row-bound file: + # THE ONE THIS LANE'S WRITES GO TO (Amendment 23, slice 2b), which + # `handoff-path` names from the row's cell — the lane's own checkout's + # copy under the opt-in — and which `lane-handoff` bound and 5c stamps. + ri_row_hf="" + ri_row_hf="$(LANES_NO_FETCH=1 "$LANES_EDIT" handoff-path "$LANE" 2>/dev/null)" && [ -f "$ri_row_hf" ] || + die "the restart handoff the row names is missing. Nothing was started." 2 [ -n "$ri_hf" ] && [ "$ri_hf" != none ] && [ -s "$ri_hf" ] && [ "$ri_hf" -ef "$ri_row_hf" ] || die "the restart intent and register name different handoffs. Nothing was started." 2 @@ -3847,6 +3903,23 @@ else session_cell="pending — set by the session's first act" fi +# AMENDMENT 23 S1 (slice 2b) — THE LANE'S OWN CHECKOUT IS MADE HERE, before the +# row, under the opt-in and the default resolution: the first write needs it, +# and every write below goes through it. A dry run plans it and makes nothing. +# A checkout that could not be made, or whose creation lock is held, is SAID +# and the launch goes on (Amendment 8(d)): each write below makes it again, or +# says for itself that it did not go through. +if [ "$LS_ROUTED" = 1 ]; then + if (( dry_run )); then + plan "$LANES_EDIT lane-checkout $LANE --create" + else + lsc_rc=0 + "$LANES_EDIT" lane-checkout "$LANE" --create >/dev/null || lsc_rc=$? + [ "$lsc_rc" = 0 ] || + note "lane $LANE's own register checkout could not be made (\`$LANES_EDIT lane-checkout $LANE --create\` exited $lsc_rc, above). The launch goes on: each write below makes it again, or says for itself that it did not go through (Amendment 23 S1, 8(d))." + fi +fi + if [ "$row_count" = 0 ]; then # AMENDMENT 13(a) — A NEW ROW IS BORN IN THE PHRASE. The state cell opened # with the bare word `STARTING` for as long as the cell was a diary; it is one @@ -4078,6 +4151,28 @@ else fi fi +# THE HANDOFF'S WRITABLE PATH, BESIDE THE ROW'S SPELLING (plan §2.7). The row's +# cell is a spelling a reader can follow, and under the opt-in a person who +# follows it lands in W, where no tool writes (Amendment 23 S5). `handoff-path` +# names the same file in the checkout this lane's writes go through, which is +# where 5c stamps it, where 5d reads the first prompt from, and where a hand +# edit belongs. A read; in a dry run it fetches nothing. +HS_PATH="" +ls_cell="$handoff" +[ "$row_count" != 1 ] || ls_cell="$(cell 7 | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' || :)" +if [ -n "$ls_cell" ]; then + hsp_rc=0; hsp_nf=0 + (( ! dry_run )) || hsp_nf=1 + HS_PATH="$(LANES_NO_FETCH="$hsp_nf" "$LANES_EDIT" handoff-path "$LANE" 2>/dev/null)" || hsp_rc=$? + if [ "$hsp_rc" = 0 ] && [ -n "$HS_PATH" ]; then + step "the lane's handoff '$ls_cell' is written through $HS_PATH" + else + HS_PATH="" + [ "$hsp_rc" = 8 ] || (( dry_run )) || + note "where this lane's handoff is written could not be read (\`$LANES_EDIT handoff-path $LANE\` exited $hsp_rc), so the first prompt below is read from the file the row's cell names" + fi +fi + # ------------------------------------------- 5b. the lane's own object log # # Amendment 7. AFTER the row, because the row is what `who` reads the lane's @@ -4260,74 +4355,34 @@ fi # end of the header block. That puts THIS resume at the top of the block of # stamps, above the ones before it. # -# AFTER 5b, so that every `lanes-edit.sh` write this run makes is already -# committed before this one touches the checkout at all. The register and the -# handoffs share one checkout, and the helper REFUSES a checkout carrying an -# uncommitted change to a tracked file it is not itself writing (Amendment 5, -# `dirty_elsewhere`). Stamp the handoff first and let its commit fail, and the -# edit made for the row is the reason the row cannot be written. -# -# AND IT IS NEVER A REASON TO REFUSE THE LAUNCH. A handoff that has moved, a -# checkout with no git, a rebase conflict, a push that hangs — each one prints -# the command that finishes the act, and the lane still starts. Rule 3's stamp -# is the RECORD of a resume, not the licence for one, and refusing the session -# would lose the conversation the stamp is about. (Rule 3 also says a lane that -# finds an existing stamp stops and reports. That is addressed to the session -# reading the resume prompt, not to the act that records the resume: the file -# already carries one line per resume and gains one more.) - -# Ruling (h)'s bound, in this script's own few lines rather than reached for in -# the helper's: every git call that touches the NETWORK runs under -# `LANES_GIT_TIMEOUT` seconds (default 60; `0` or a non-number turns the bound -# off), so a hung remote costs the launch a minute and never the session. No -# `timeout` on PATH is not a reason to skip the call — it is a reason to make -# it unbounded, which is what this did before the bound existed. -HS_TIMED_OUT=0 -hs_git_net() { - local rc=0 budget="${LANES_GIT_TIMEOUT:-60}" - HS_TIMED_OUT=0 - case "$budget" in - ''|0|*[!0-9]*) "${LANES_GIT:-git}" "$@" || rc=$? ;; - *) - if command -v timeout >/dev/null 2>&1; then - timeout -k 5 "$budget" "${LANES_GIT:-git}" "$@" || rc=$? - case "$rc" in 124|137) HS_TIMED_OUT=1 ;; esac - else - "${LANES_GIT:-git}" "$@" || rc=$? - fi ;; - esac - return "$rc" -} - -# F-B7 — WHICH CASE THE READER IS IN, said rather than assumed. A stamp that is -# committed here and not pushed is carried out by the next `lanes-edit.sh` write -# ONLY where the handoff lives in the register's own checkout: that write pushes -# the BRANCH, so it takes the orphaned commit with it. `hs_resolve` also accepts -# an absolute path and a workspace root, either of which can land outside -# `$LANES_REPO` — and nothing ever pushes a commit made there, because no -# `lanes-edit.sh` write goes out of it. Promising a carry that will not happen -# is worse than printing the command, so the note says which it is. -hs_carry_note() { - # UNDER `LANES_ROUTE_WRITES=1` NO WRITE GOES OUT OF W (Amendment 23 S2; the - # review of #282, SHOULD 6): with the default resolution each one goes through - # its lane's own checkout, so none will ever push a commit made here. - if [ "${LANES_ROUTE_WRITES:-}" = 1 ] && [ "$LANES_REG_DEFAULT" = 1 ] && - [ -n "${LANES_REPO:-}" ] && [ "$hs_repo" = "$LANES_REPO" ]; then - note " (this handoff is in W ($hs_repo), and under LANES_ROUTE_WRITES=1 every lanes-edit.sh write goes through its lane's own checkout instead, so none will push this commit: the command above is the whole of the recovery.)" - elif [ -n "${LANES_REPO:-}" ] && [ "$hs_repo" = "$LANES_REPO" ]; then - note " (this handoff is in the register's own checkout, so the next lanes-edit.sh write out of it pushes the branch and carries this commit with it.)" - else - note " (this handoff is NOT in the register's checkout ($hs_repo), so no lanes-edit.sh write will ever push it: the command above is the whole of the recovery.)" - fi - return 0 -} - -# The workspace repository, which is where handoffs live (Amendment 4(a)) and, -# since Amendment 9(a), where the register lives too. THIS USED TO BE A SECOND -# READER of the same file with a looser rule — it took any `path:` at any -# indentation and checked nothing — and Amendment 9(a) is explicit that a -# second way to find one thing is a second answer. It now defers to the one -# resolver above, which is the same in all four helpers. +# AFTER 5b, AND THROUGH THE SAME CHECKOUT (Amendment 23, slice 2b). The stamp is +# ONE `lanes-edit.sh write-handoff --stamp`. Under the lock of the checkout +# every write of this lane goes through — its own `-lanes//` under the +# opt-in, an explicit `LANES_REPO` in place, W where the opt-in is off — it +# fast-forwards that checkout (S3), reads the row's handoff cell from what it +# fetched and the file in that checkout, splices this line in and proves it +# (this script's own splice, moved there unchanged), replaces the file +# atomically under a restart's fences, commits it by pathspec with the `Lane:` +# trailer and pushes it (S4), replaying the splice on origin's new tip where a +# peer won the race. Its one stdout line is the file it wrote and that file's +# digest. It used to be this script's own: published under W's lock, then +# added, committed, pulled and pushed in W outside any lock (lane +# openRepoTools-3's finding 282, the first of three such writers), and the +# notes that said which later write would push a stranded stamp went with it: +# none does (S3). +# +# AND IT IS NEVER A REASON TO REFUSE THE LAUNCH. A handoff that is not there, a +# checkout S3 refuses, a push that is refused or never confirmed — each one +# prints what was left and the command that finishes the act, and the lane +# still starts. Rule 3's stamp is the RECORD of a resume, not the licence for +# one, and refusing the session would lose the conversation the stamp is about. +# (Rule 3 also says a lane that finds an existing stamp stops and reports. That +# is addressed to the session reading the resume prompt, not to the act that +# records the resume: the file already carries one line per resume and gains +# one more.) THE ONE EXCEPTION IS A SUPERVISED RESTART'S OWN FENCES. The file +# its intent bound is this launch's first prompt, so a write that lost the +# restart's ownership (7), or a bound file that no longer digests as the +# restart bound it, stops the launch here, as it always did. # The handoff path is field 7 of the PUBLISHED row: the register's header is # `| lane | session id | workstation / env / user | started (UTC) | objects @@ -4344,181 +4399,52 @@ hs_carry_note() { # rule R-A8-2 gives — and 1 for an agent whose id is its own kind of id, because # refusing that stamp would leave Rule 3 unstamped for every launch this # amendment exists to make. +hs_bound_unchanged() { # the restart's bound file still digests as the restart bound it + [ "${ri_expected_digest:-none}" != none ] || return 0 + [ -n "$ri_hf" ] && [ "$ri_hf" != none ] && [ "$(handoff_digest_of "$ri_hf")" = "$ri_expected_digest" ] +} if [ "$lane_verb" = RESUMED ] && [ "$row_count" = 1 ] && (( stamp_ok )); then hs_cell="$(cell 7 | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' || :)" hs_stamp="RESUMED by $stamp_who (lane $LANE) at $utc — lane-start on $ws" hs_subject="handoff($LANE@$ws): RESUMED by $stamp_who" - hs_trailer="Lane: $LANE ($ws)" - hs_file="" + hs_args=(write-handoff "$LANE" --stamp "$hs_stamp" --subject "$hs_subject") + hs_fix="LANES_LANE=$LANE $(quoted "$LANES_EDIT" "${hs_args[@]}")" + # UNDER A SUPERVISED RESTART the write is fenced on the intent as 5a left it + # — `starting`, with this launch's transcript reserved — and on the digest + # the restart bound: the pre-image must be it, and the stamped file's digest + # is reserved as the intent's prepared digest before its bytes land, in the + # helper and under its lock (slice 2a, decision 7). + if [ "$ri_active" = 1 ]; then + hs_args+=(--restart "$ri_op" "$ri_gen" "$ri_attempt" starting "$lane_sid" --expected-digest "${ri_expected_digest:-none}") + fi if [ -z "$hs_cell" ]; then step "the row names no handoff path, so there is no Rule 3 stamp to write" - elif ! hs_file="$(hs_resolve "$hs_cell")"; then - hs_ws_root="$(hs_workspace_root 2>/dev/null || :)" - note "the handoff the row points at was NOT found, so Rule 3's stamp is not written: '$hs_cell'" - note " looked under ${LANES_REPO:-} and ${hs_ws_root:-}" - note " STAMP IT AS THIS SESSION'S FIRST ACT — this line under the handoff's header block:" - note " $hs_stamp" - note " then: git -C commit -m \"$hs_subject\" -m \"$hs_trailer\" -- " + elif (( dry_run )); then + plan "LANES_LANE=$LANE $(quoted "$LANES_EDIT" "${hs_args[@]}")" else - # A handoff may be reached through the estate's `handoffs` symlink, so the - # path git is given is the REAL one: a pathspec that leaves the repository - # lexically is a pathspec git refuses, however plainly it is inside it. - # `cd -P` AND NOT `readlink -f`, for the reason `real_of` gives above and - # `lane-handoff` gives one file over (Copilot round 3 on openRepoTools#47): - # an unresolved path here is a `hs_rel` git refuses as a pathspec, and the - # stamp is then written and never committed. - hs_dirreal="$( CDPATH=''; cd -P -- "$(dirname -- "$hs_file")" 2>/dev/null && pwd -P )" || - hs_dirreal="$(dirname -- "$hs_file")" - hs_real="$hs_dirreal/$(basename -- "$hs_file")" - # Resolve a final-component symlink too; atomic publication replaces its - # real target and leaves the estate link intact. - hs_links=0 - while [ -L "$hs_real" ]; do - hs_links=$((hs_links + 1)) - [ "$hs_links" -lt 40 ] || die "the handoff symlink chain cannot be resolved. Nothing was launched." 2 - hs_link="$(readlink "$hs_real")" || die "the handoff symlink could not be read." 2 - case "$hs_link" in /*) hs_real="$hs_link" ;; *) hs_real="$(dirname -- "$hs_real")/$hs_link" ;; esac - hs_dirreal="$( CDPATH=''; cd -P -- "$(dirname -- "$hs_real")" && pwd -P )" || die "the handoff target directory is missing." 2 - hs_real="$hs_dirreal/$(basename -- "$hs_real")" - done - hs_repo="$(git -C "$(dirname -- "$hs_real")" rev-parse --show-toplevel 2>/dev/null || :)" - hs_rel="$hs_real" - [ -n "$hs_repo" ] && hs_rel="${hs_real#"$hs_repo"/}" - hs_fix="git -C ${hs_repo:-} commit -m \"$hs_subject\" -m \"$hs_trailer\" -- $hs_rel" - hs_at="" - if [ -r "$hs_real" ] && [ -s "$hs_real" ]; then - hs_at="$(awk 'NR > 1 && /^[[:space:]]*$/ { print NR; exit }' "$hs_real" || :)" - if [ -z "$hs_at" ]; then - hs_at=1 - note "$hs_rel has no blank line after line 1, so it has no header block to stamp under — the stamp goes directly under line 1" - fi - fi - if [ -z "$hs_at" ]; then - note "the handoff the row points at is empty or unreadable, so Rule 3's stamp is not written: $hs_file" + hs_out=""; hs_rc=0 + hs_out="$(LANES_LANE="$LANE" "$LANES_EDIT" "${hs_args[@]}")" || hs_rc=$? + hs_line="$(printf '%s\n' "$hs_out" | awk -F'\t' 'NF >= 2 && $1 != "" { l = $0 } END { print l }')" + if [ -n "$hs_line" ]; then + hs_written="${hs_line%% *}" + # THE DIGEST THE RESTART NOW EXPECTS is the stamped file's, which the + # helper reserved in the intent before the bytes: 5d reads that file. + if [ "$ri_active" = 1 ]; then ri_expected_digest="${hs_line##* }"; fi + case "$hs_rc" in + 0) step "handoff stamped, committed and pushed: $hs_written: $hs_stamp" ;; + 3) note "Rule 3's stamp is written in $hs_written and its commit pushed, and origin did not confirm it (\`write-handoff\` exited 3; its words above name the commit): look before writing it again, because a stamp written twice is two resumes in the one document this protocol never rewrites." ;; + 6) note "Rule 3's stamp is written in $hs_written and committed, and the commit is KEPT on HEAD of that checkout, not pushed (\`write-handoff\` exited 6; its words above name it). No later write publishes it, and each one refuses until it is published (Amendment 23 S3): once those words are answered, LANES_LANE=$LANE $LANES_EDIT commit \"\", the publish act." ;; + *) note "Rule 3's stamp is written in $hs_written and was NOT published (\`write-handoff\` exited $hs_rc; its words above say what is left). The launch is not refused for it." ;; + esac + elif [ "$ri_active" = 1 ] && [ "$hs_rc" = 7 ]; then + die "this restart attempt lost ownership of its handoff before the resume stamp (\`write-handoff\` exited 7; its words are above). Nothing was launched." 2 + elif [ "$ri_active" = 1 ] && ! hs_bound_unchanged; then + die "the restart handoff changed before its owned resume stamp. Nothing was launched." 2 + else + note "Rule 3's stamp was NOT written (\`write-handoff\` exited $hs_rc; its words are above), and the launch is not refused for it: the stamp is the record of this resume, not its licence." note " STAMP IT AS THIS SESSION'S FIRST ACT — this line under the handoff's header block:" note " $hs_stamp" - note " then: $hs_fix" - elif (( dry_run )); then - plan "stamp $hs_rel at line $((hs_at + 1)): $hs_stamp" - plan "$hs_fix" - plan " then git -C ${hs_repo:-} pull --rebase origin ${LANES_BRANCH:-main} && … push origin ${LANES_BRANCH:-main}" - else - # THE FILE IS SPLICED, NEVER REWRITTEN. A handoff is the one document in - # this protocol whose whole purpose is to be read by a later session, and - # the act that deleted two lanes' rows on 2026-09-08 was a file written - # whole from what somebody remembered of it. So: read it, put ONE line - # in, write the rest back unchanged — and prove it before it lands. The - # added line must be exactly the stamp, at exactly the ruled position, - # and taking that line back out again must give the original file byte - # for byte. If it does not, nothing is written at all: that is also what - # catches a file whose last line has no newline, where `head` would glue - # the stamp on to the end of it. - hs_tmpd="$(mktemp -d)" - hs_pre="$hs_tmpd/pre"; hs_out="$hs_tmpd/out" - if ! cat -- "$hs_real" > "$hs_pre" 2>/dev/null; then - note "the handoff the row points at could not be read, so Rule 3's stamp is not written: $hs_file" - note " STAMP IT AS THIS SESSION'S FIRST ACT — this line under the handoff's header block:" - note " $hs_stamp" - note " then: $hs_fix" - else - if [ "$ri_active" = 1 ] && [ "$ri_expected_digest" != none ] && - [ "$(handoff_digest_of "$hs_pre")" != "$ri_expected_digest" ]; then - rm -rf -- "$hs_tmpd" - die "the restart handoff changed before its owned resume stamp. Nothing was launched." 2 - fi - { head -n "$hs_at" -- "$hs_pre" - printf '%s\n' "$hs_stamp" - tail -n "+$((hs_at + 1))" -- "$hs_pre" - } > "$hs_out" - if [ "$(sed -n -e "$((hs_at + 1))p" "$hs_out")" = "$hs_stamp" ] && - { head -n "$hs_at" -- "$hs_out"; tail -n "+$((hs_at + 2))" -- "$hs_out"; } | cmp -s - "$hs_pre" - then - if [ "$ri_active" = 1 ]; then - ri_post_digest="$(handoff_digest_of "$hs_out")" - # Persist both sides of this exact, verified stamp splice before - # publishing bytes. Interruption on either side remains retryable. - if ! LANES_NO_FETCH=1 "$LANES_EDIT" set-restart-intent "$LANE" starting \ - --expect starting --expect-operation "$ri_op" \ - --expect-generation "$ri_gen" --expect-attempt "$ri_attempt" \ - --expect-new-transcript "$lane_sid" --digest "$ri_expected_digest" --prepared-digest "$ri_post_digest" \ - --reason 'owned handoff stamp prepared' >/dev/null; then - rm -rf -- "$hs_tmpd" - die "this attempt could not reserve its handoff stamp. Nothing was launched." 2 - fi - ri_expected_digest="$ri_post_digest" - fi - hs_publish_args=(publish-handoff "$LANE" "$hs_real" "$hs_out" --expect-digest "$(handoff_digest_of "$hs_pre")") - if [ "$ri_active" = 1 ]; then - hs_publish_args+=(--operation "$ri_op" --generation "$ri_gen" --attempt "$ri_attempt" --state starting --transcript "$lane_sid") - fi - if ! LANES_NO_FETCH=1 "$LANES_EDIT" "${hs_publish_args[@]}" >/dev/null; then - rm -rf -- "$hs_tmpd" - die "the handoff stamp could not be published atomically under its writer lock. Nothing was launched." 2 - fi - step "handoff stamped at line $((hs_at + 1)) of $hs_rel: $hs_stamp" - if [ "${LANES_NO_GIT:-0}" = 1 ]; then - note "LANES_NO_GIT=1 — the stamp is written and NOT committed:" - note " $hs_fix" - elif [ -z "$hs_repo" ]; then - note "$hs_real is not inside a git checkout, so the stamp cannot be committed from here — it is written, and nothing outside this machine knows it." - else - # AMENDMENT 4(d) — one commit per write, an EXPLICIT pathspec, and - # the `Lane:` trailer. Never `git add -A` and never a bare `git - # commit`: this checkout is shared with every other lane on this - # workstation, and a write left uncommitted is captured by the next - # lane's commit with its attribution lost. - hs_rc=0 - "${LANES_GIT:-git}" -C "$hs_repo" add -- "$hs_rel" 1>&2 || hs_rc=$? - if [ "$hs_rc" = 0 ]; then - "${LANES_GIT:-git}" -C "$hs_repo" commit -q -m "$hs_subject" -m "$hs_trailer" -- "$hs_rel" 1>&2 || hs_rc=$? - fi - if [ "$hs_rc" != 0 ]; then - note "the stamp is written but NOT committed (git exit $hs_rc) — an uncommitted write is captured by the next lane's commit and its attribution is lost (Amendment 4(d)). Commit it yourself:" - note " $hs_fix" - else - step "handoff stamp committed: $hs_subject" - hs_branch="${LANES_BRANCH:-main}" - if ! hs_git_net -C "$hs_repo" pull --rebase -q origin "$hs_branch" 1>&2; then - "${LANES_GIT:-git}" -C "$hs_repo" rebase --abort >/dev/null 2>&1 || : - if [ "$HS_TIMED_OUT" = 1 ]; then - note "the pull before the push TIMED OUT after ${LANES_GIT_TIMEOUT:-60}s (ruling (h): the launch does not wait on a hung remote) — the stamp is a local commit here and was not pushed." - else - note "the pull before the push did not apply (a conflict, or no reachable origin) — the rebase is aborted, the checkout is clean, and the stamp is a local commit here that was not pushed." - fi - # RV-B3 — LOOK FIRST, because an aborted pull does NOT mean the - # commit stayed here. Observed live 2026-09-12T23:04Z: this - # lane's handoff commit hit an abort on a peer lane's untracked - # log file, and the peer's own helper then rebased and pushed — - # carrying this lane's local commit `59a2576` out to origin - # underneath its own `f053245`. A reader who ran the re-push - # blind at that point lands the same stamp twice, in the one - # document this protocol cannot afford to rewrite. The - # push-rejected branch below has always said to look; this one - # promised a recovery that is sometimes already done. - note " git -C $hs_repo fetch origin $hs_branch && git -C $hs_repo log --oneline origin/$hs_branch -3 # a peer's push may already carry it" - note " git -C $hs_repo pull --rebase origin $hs_branch && git -C $hs_repo push origin $hs_branch # only if it is not there" - hs_carry_note - elif ! hs_git_net -C "$hs_repo" push -q origin "$hs_branch" 1>&2; then - if [ "$HS_TIMED_OUT" = 1 ]; then - note "the push TIMED OUT after ${LANES_GIT_TIMEOUT:-60}s (ruling (h)) — a hung push often LANDED and the connection did not close, so look before pushing again." - else - note "the push was rejected — the stamp is a local commit here and other workstations cannot see it yet." - fi - note " git -C $hs_repo fetch origin $hs_branch && git -C $hs_repo log --oneline origin/$hs_branch -3" - note " git -C $hs_repo push origin $hs_branch # only if it is not there" - hs_carry_note - else - step "handoff stamp pushed to origin/$hs_branch" - fi - fi - fi - else - note "the splice of Rule 3's stamp into $hs_rel did not prove out (one line added, none changed), so NOTHING was written to it. Add this line under its header block yourself:" - note " $hs_stamp" - note " then: $hs_fix" - fi - fi - rm -rf -- "$hs_tmpd" + note " with: $hs_fix" fi fi fi @@ -4551,8 +4477,22 @@ if (( first_prompt )); then else fp_cell="$handoff" fi + # READS FOLLOW THE WRITE (Amendment 23, slice 2b): the file 5c wrote, in the + # checkout this lane's writes go through, and not W's copy, which nothing + # here brought up to date. Under a supervised restart it is the only file + # there is, because its intent bound it. For any other launch, where that + # file is not there, the copy the row's cell names is read, and said. fp_file="" - [ -n "$fp_cell" ] && fp_file="$(hs_resolve "$fp_cell" 2>/dev/null || :)" + if [ -n "$fp_cell" ]; then + if [ -n "$HS_PATH" ] && [ -f "$HS_PATH" ]; then + fp_file="$HS_PATH" + elif [ "$ri_active" != 1 ] || (( ! fresh )); then + fp_file="$(hs_resolve "$fp_cell" 2>/dev/null || :)" + if [ -n "$fp_file" ] && [ -n "$HS_PATH" ]; then + note "this lane's own copy of its handoff is not at $HS_PATH, so the first prompt is read from $fp_file, which may be behind it" + fi + fi + fi if [ "$ri_active" = 1 ] && (( fresh )); then [ -n "$fp_file" ] && [ "$fp_file" -ef "$ri_hf" ] || die "the delivered restart prompt is not the intent-bound handoff. Nothing was launched." 2 @@ -4682,6 +4622,13 @@ lane_daily_report() { } (( dry_run )) || lane_daily_report || : +# ------------------------- 5f. W, brought up to this launch's writes (S5) +# +# The second of the two refreshes (see `ls_refresh`): the next reader of W — +# the next `lane-start` of any lane, `lanes`, `status` — finds this launch's row, +# log line and stamp there. A refusal is a note, and the launch goes on. +ls_refresh "after this launch's writes" + # ---------------------------------------------------------------- 6. launch # # `cmd` was decided in 5a, before the row was written. diff --git a/tests/test_a23_lane_start.py b/tests/test_a23_lane_start.py new file mode 100644 index 0000000..c8c126b --- /dev/null +++ b/tests/test_a23_lane_start.py @@ -0,0 +1,301 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Amendment 23, act 2's slice 2b: `lane-start` writes only through the lane's +own register checkout, and W moves only by fast-forward. + +Until this slice `lane-start` wrote the handoff's Rule 3 stamp itself: it +published it under W's lock and then added, committed, pulled and pushed it in +W with no lock at all (lane openRepoTools-3's finding 282, its first writer). +Under the default resolution the lane's other writes already went through +`-lanes//` (slice 1a), so the stamp was the one write left in W, and +it read W's copy of the handoff, which nothing brought up to date. + +Now: + * `refresh-workspace` fast-forwards W before the first read and again once + the writes are done, and the row is read from W after the first; + * the lane's checkout is made at "5. the row", and its writable handoff path + is printed beside the row's spelling; + * the stamp is ONE `write-handoff --stamp` call, under the checkout's lock + and S3/S4, and its exit is a note: the launch is refused only by a + restart's own fences; + * the first prompt is read from the file that call writes. + +Each case drives a real `lane-start` in `test_lane_start_row_stamp`'s sandbox +with the default resolution (no `LANES_REPO`, no `LANES_FILE`) and the opt-in +`LANES_ROUTE_WRITES=1`. A "peer" is another clone of the sandbox's origin that +commits and pushes, as another workstation's lane does. +""" + +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +from test_a23_lane_start_read import default_resolution, rows +from test_lane_start_row_stamp import LANE, Sandbox, _checkout_of_length, pytestmark, short_root # noqa: F401 + +HANDOFF = "Lane: repoZ-1 — single-use resume prompt\n\n# state\n\nthe lane's own words\n" +FF = "merge origin/main: Fast-forward" +PEER_SID = "bbbb0001-2222-4000-8000-bbbb00012222" + + +# ------------------------------------------------------------------ helpers + +def _quiet(box: Sandbox, repo: Path) -> None: + for key, value in (("gc.auto", "0"), ("maintenance.auto", "false")): + box.git("-C", str(repo), "config", key, value) + + +def _origin(box: Sandbox, *args: str) -> str: + return box.git("--git-dir", str(box.root / "origin.git"), *args) + + +def _published(box: Sandbox, rel: str) -> str: + return _origin(box, "show", f"main:{rel}") + + +def _tip(box: Sandbox) -> str: + return _origin(box, "rev-parse", "main").strip() + + +def _cell(box: Sandbox) -> str: + """The handoff cell of the lane's published row.""" + found = [r for r in _published(box, "lanes/LANES.md").splitlines() + if r.startswith(f"| `{LANE}`")] + assert len(found) == 1, found + return found[0].split("|")[6].strip() + + +def _peer(box: Sandbox) -> Path: + p = box.root / "peer" + if not p.exists(): + box.git("clone", "-q", str(box.root / "origin.git"), str(p)) + _quiet(box, p) + box.git("-C", str(p), "fetch", "-q", "origin", "main") + box.git("-C", str(p), "merge", "-q", "--ff-only", "origin/main") + return p + + +def _peer_push(box: Sandbox, files: dict[str, str], msg: str) -> None: + p = _peer(box) + for rel, text in files.items(): + (p / rel).parent.mkdir(parents=True, exist_ok=True) + (p / rel).write_text(text, encoding="utf-8") + box.git("-C", str(p), "add", "--", *files) + box.git("-C", str(p), "commit", "-q", "-m", msg) + box.git("-C", str(p), "push", "-q", "origin", "HEAD:main") + + +def _edit(box: Sandbox, *args: str, env: dict | None = None) -> subprocess.CompletedProcess: + return subprocess.run([str(box.bin / "lanes-edit.sh"), *args], env=env or box.env, + capture_output=True, text=True, timeout=180, check=False) + + +def _reflog(box: Sandbox) -> list[str]: + return box.git("-C", str(box.wip), "reflog", "--format=%gs", "HEAD").splitlines() + + +def _boxed(tmp_path: Path, short_root: Path) -> tuple[Sandbox, Path, Path]: + """The sandbox with the default resolution and the opt-in, W kept from gc.""" + box = Sandbox(tmp_path) + checkout = default_resolution(box) + _quiet(box, box.wip) + for key, value in (("gc.auto", "0"), ("maintenance.auto", "false")): + _origin(box, "config", key, value) + lane_dir = _checkout_of_length(short_root, None) + box.checkout(lane_dir) + return box, checkout, lane_dir + + +def _started(tmp_path: Path, short_root: Path) -> tuple[Sandbox, Path, Path, list[str]]: + """A new lane's first start, with W's reflog as it stood before it.""" + box, checkout, lane_dir = _boxed(tmp_path, short_root) + before = _reflog(box) + first = box.start("--dir", str(lane_dir), window_name="claude") + assert first.returncode == 0, first.stderr + assert (checkout / ".git").is_dir(), "the first start made the lane's checkout" + return box, checkout, lane_dir, before + + +def _assert_W_only_fast_forwarded(box: Sandbox, before: list[str]) -> None: + after = _reflog(box) + assert after[len(after) - len(before):] == before, (before, after) + new = after[:len(after) - len(before)] + assert all(entry == FF for entry in new), new + assert box.git("-C", str(box.wip), "status", "--porcelain") == "", "W is clean" + + +def _assert_nothing_unpublished(box: Sandbox, checkout: Path) -> None: + cherry = box.git("-C", str(checkout), "cherry", "origin/main", "HEAD") + assert not [c for c in cherry.splitlines() if c.startswith("+")], cherry + + +# -------------------------------------------------------------------- cases + +def test_launch_creates_checkout_and_writes_only_there(tmp_path, short_root): + box, checkout, _lane_dir, before = _started(tmp_path, short_root) + cell = _cell(box) + _peer_push(box, {cell: HANDOFF}, "the lane's handoff, written elsewhere") + + second = box.start("--fresh", window_name=LANE) + assert second.returncode == 0, second.stderr + + register = _published(box, "lanes/LANES.md") + assert sum(1 for r in register.splitlines() if r.startswith(f"| `{LANE}`")) == 1 + row = [r for r in register.splitlines() if r.startswith(f"| `{LANE}`")][0] + assert "LIVE · " in row and "RESUMED by " in row, row + assert f"RESUMED — lane {LANE}, session " in _published(box, f"lanes/log/{LANE}.md") + stamped = _published(box, cell) + assert f"(lane {LANE}) at " in stamped and stamped.splitlines()[2].startswith("RESUMED by "), stamped + assert _origin(box, "log", "-1", "--format=%s%n%b", "main").startswith(f"handoff({LANE}@Eagle): RESUMED by ") + assert f"Lane: {LANE}" in _origin(box, "log", "-1", "--format=%b", "main") + + _assert_W_only_fast_forwarded(box, before) + assert box.git("-C", str(box.wip), "rev-parse", "HEAD").strip() == _tip(box), "W ends on origin's tip" + _assert_nothing_unpublished(box, checkout) + + +def test_launch_fast_forwards_W_before_and_after(tmp_path, short_root): + box, checkout, lane_dir = _boxed(tmp_path, short_root) + made = _edit(box, "lane-checkout", LANE, "--create") + assert made.returncode == 0, made.stderr + before = _reflog(box) + # Another workstation's start of this lane: its row, and its handoff. + register = _published(box, "lanes/LANES.md") + row = (f"| `{LANE}` | harness `{PEER_SID}` (minted by lane-start, 2026-10-22T00:00:00Z) | " + f"Eagle / default / peer | 2026-10-22T00:00Z | none | handoffs/repoZ/peer.md | " + f"LIVE · 2026-10-22T00:00:00Z · STARTED by {PEER_SID} (lane {LANE}) — lane-start on Eagle: row created |") + _peer_push(box, {"lanes/LANES.md": register + row + "\n", "handoffs/repoZ/peer.md": HANDOFF}, + "a peer's start of the lane") + + start = box.start("--dir", str(lane_dir), "--fresh", window_name=LANE) + assert start.returncode == 0, start.stderr + assert "ALREADY has a row" not in start.stderr, start.stderr + assert len(rows(box.wip / "lanes" / "LANES.md")) == 1, "W was brought up to the peer's row first" + assert box.git("-C", str(box.wip), "rev-parse", "HEAD").strip() == _tip(box), "and to the stamp after" + assert _origin(box, "log", "-1", "--format=%s", "main").startswith(f"handoff({LANE}@Eagle): RESUMED by ") + _assert_W_only_fast_forwarded(box, before) + _assert_nothing_unpublished(box, checkout) + + +def test_launch_not_refused_when_S3_refuses(tmp_path, short_root): + box, checkout, _lane_dir, _before = _started(tmp_path, short_root) + cell = _cell(box) + _peer_push(box, {cell: HANDOFF}, "the lane's handoff, written elsewhere") + assert _edit(box, "refresh-workspace").returncode == 0 + foreign = checkout / "handoffs" / "repoQ" / "foreign.md" + foreign.parent.mkdir(parents=True, exist_ok=True) + foreign.write_text("a person's file, staged and never committed\n", encoding="utf-8") + box.git("-C", str(checkout), "add", "--", "handoffs/repoQ/foreign.md") + tip = _tip(box) + + start = box.start("--fresh", window_name=LANE) + assert start.returncode == 0, start.stderr + assert _tip(box) == tip, "nothing was published, from the lane's checkout or from W" + assert "RESUMED by " not in _published(box, cell) + assert "the stamp line it would have written under the handoff's header block: RESUMED by " in start.stderr, start.stderr + assert "handoffs/repoQ/foreign.md" in box.git("-C", str(checkout), "diff", "--cached", "--name-only") + assert box.git("-C", str(box.wip), "status", "--porcelain") == "", "W took no write" + + +def test_the_1312Z_shape_all_three_writes_pushed(tmp_path, short_root): + """The shape of 2026-10-17T13:12Z: the lane's checkout clean, nothing + ahead and ten commits behind, one of them a change to the row beside the + lane's, one the lane's handoff. The row state, the log line and the stamp + all land on origin.""" + box, checkout, _lane_dir, _before = _started(tmp_path, short_root) + cell = _cell(box) + register = _published(box, "lanes/LANES.md") + other = (f"| `repoZ-2` | harness `{PEER_SID}` | Eagle / default / peer | 2026-10-22T00:00Z | none | " + "handoffs/repoZ/two.md | LIVE · 2026-10-22T00:00:00Z · STARTED by peer |") + _peer_push(box, {"lanes/LANES.md": register + other + "\n"}, "repoZ-2's row") + changed = other.replace("STARTED by peer", "PAUSED by peer") + _peer_push(box, {"lanes/LANES.md": register + changed + "\n"}, "repoZ-2's state") + _peer_push(box, {cell: HANDOFF}, "the lane's handoff") + for n in range(7): + _peer_push(box, {f"handoffs/repoQ/note-{n}.md": f"note {n}\n"}, f"another lane's note {n}") + box.git("-C", str(checkout), "fetch", "-q", "origin", "main") + assert box.git("-C", str(checkout), "rev-list", "--left-right", "--count", + "HEAD...origin/main").split() == ["0", "10"] + assert box.git("-C", str(checkout), "status", "--porcelain") == "" + + start = box.start("--fresh", window_name=LANE) + assert start.returncode == 0, start.stderr + published = _published(box, "lanes/LANES.md") + row = [r for r in published.splitlines() if r.startswith(f"| `{LANE}`")] + assert len(row) == 1 and "RESUMED by " in row[0], row + assert changed in published, "the peer's change to the row beside it is kept" + assert f"RESUMED — lane {LANE}, session " in _published(box, f"lanes/log/{LANE}.md") + assert _published(box, cell).splitlines()[2].startswith("RESUMED by "), _published(box, cell) + _assert_nothing_unpublished(box, checkout) + + +def test_first_prompt_reads_the_checkout_copy(tmp_path, short_root): + box, checkout, _lane_dir, _before = _started(tmp_path, short_root) + cell = _cell(box) + _peer_push(box, {cell: HANDOFF}, "the lane's handoff, written elsewhere") + assert _edit(box, "refresh-workspace").returncode == 0 + stray = box.wip / cell + stray.write_text(HANDOFF.replace("# state", "# state STRAY-W-EDIT"), encoding="utf-8") + + start = box.start("--fresh", window_name=LANE) + assert start.returncode == 0, start.stderr + assert "RESUMED by " in start.stdout and f"(lane {LANE}) at " in start.stdout, start.stdout + assert "STRAY-W-EDIT" not in start.stdout, start.stdout + assert "STRAY-W-EDIT" not in _published(box, cell), "W's stray never reaches origin" + assert "STRAY-W-EDIT" in stray.read_text(encoding="utf-8"), "and W's stray is a person's to settle" + + +def test_writable_handoff_path_printed(tmp_path, short_root): + box, checkout, _lane_dir, _before = _started(tmp_path, short_root) + cell = _cell(box) + second = box.start(window_name=LANE) + assert second.returncode == 0, second.stderr + assert f"{checkout}/{cell}" in second.stderr, second.stderr + assert cell in second.stderr + + +def test_dry_run_creates_nothing_and_leaves_W(tmp_path, short_root): + box, checkout, lane_dir = _boxed(tmp_path, short_root) + box.env.pop("LANES_ROUTE_WRITES") + first = box.start("--dir", str(lane_dir), window_name="claude") + assert first.returncode == 0, first.stderr + assert not checkout.parent.exists(), "without the opt-in the row is W's" + box.env["LANES_ROUTE_WRITES"] = "1" + _peer_push(box, {_cell(box): HANDOFF}, "the lane's handoff, written elsewhere") + head = box.git("-C", str(box.wip), "rev-parse", "HEAD") + before = _reflog(box) + + plan = box.start("--fresh", "--dry-run", window_name=LANE) + assert plan.returncode == 0, plan.stderr + assert not checkout.exists() and not checkout.parent.exists(), "a dry run makes no lane checkout" + assert box.git("-C", str(box.wip), "rev-parse", "HEAD") == head, "W did not move" + assert _reflog(box) == before + for planned in ("refresh-workspace", f"lane-checkout {LANE} --create", f"write-handoff {LANE} --stamp"): + assert any("PLAN" in line and planned in line for line in plan.stderr.splitlines()), (planned, plan.stderr) + + +def test_publish_failure_no_longer_refuses_a_plain_resume(tmp_path, short_root): + """`lane-start:4455` died 2 after the row and the log were written, when + `publish-handoff` failed, a lock wait over 60 s included. The stamp is the + record of the resume, never its licence.""" + box, _checkout, _lane_dir, _before = _started(tmp_path, short_root) + cell = _cell(box) + _peer_push(box, {cell: HANDOFF}, "the lane's handoff, written elsewhere") + assert _edit(box, "refresh-workspace").returncode == 0 + wrapper = box.root / "refusing-lanes-edit.sh" + wrapper.write_text(f"""#!/usr/bin/env bash +case "${{1-}}" in publish-handoff|write-handoff) echo "test: $1 refused" >&2; exit 1 ;; esac +exec '{box.bin / "lanes-edit.sh"}' "$@" +""", encoding="utf-8") + wrapper.chmod(0o755) + + start = subprocess.run([str(box.bin / "lane-start"), "repoZ", "1", "--no-launch", "--fresh"], + env=dict(box.env, LANES_EDIT=str(wrapper), FAKE_TMUX_WINDOW_NAME=LANE), + capture_output=True, text=True, timeout=180, check=False) + assert start.returncode == 0, start.stderr + assert "test: write-handoff refused" in start.stderr, start.stderr + assert "RESUMED by " in start.stderr and "write-handoff" in start.stderr + assert "Nothing was launched" not in start.stderr + assert "RESUMED by " not in _published(box, cell) + assert os.path.exists(box.wip / cell) diff --git a/tests/test_a23_lane_start_read.py b/tests/test_a23_lane_start_read.py index ab14fd4..f50b184 100644 --- a/tests/test_a23_lane_start_read.py +++ b/tests/test_a23_lane_start_read.py @@ -16,6 +16,12 @@ review of opensoft/openRepoTools#282, MUST 2); one case leaves it out. The sandbox is `test_lane_start_row_stamp`'s, with its explicit seam taken back out. + +Since slice 2b `lane-start` reads the row from W again, but only after +`refresh-workspace` has fast-forwarded it, and makes the lane's checkout at +"5. the row" (`tests/test_a23_lane_start.py`). So W's working tree now moves, +by fast-forward and nothing else, and a checkout that cannot be made no longer +changes which register the start reads. `lane-end` keeps slice 1a's read. """ from __future__ import annotations @@ -46,29 +52,45 @@ def rows(register: Path) -> list[str]: if r.startswith(f"| `{LANE}`")] +def W_reflog(box: Sandbox) -> list[str]: + return box.git("-C", str(box.wip), "reflog", "--format=%gs", "HEAD").splitlines() + + +def assert_W_moved_only_by_fast_forward(box: Sandbox, before: list[str]) -> None: + """W took no write: its HEAD moved only by `refresh-workspace`'s + fast-forward, and its working tree is clean (Amendment 23 S5).""" + after = W_reflog(box) + new = after[:len(after) - len(before)] + assert after[len(new):] == before, (before, after) + assert all(entry == "merge origin/main: Fast-forward" for entry in new), new + assert box.git("-C", str(box.wip), "status", "--porcelain") == "", "W is clean" + + def started(tmp_path: Path, short_root: Path) -> tuple[Sandbox, Path]: """A NEW lane's first start: its row is added through its own checkout.""" box = Sandbox(tmp_path) checkout = default_resolution(box) lane_dir = _checkout_of_length(short_root, None) box.checkout(lane_dir) + before = W_reflog(box) first = box.start("--dir", str(lane_dir), window_name="claude") assert first.returncode == 0, first.stderr assert (checkout / ".git").is_dir(), "the first write made the lane's checkout" assert len(rows(checkout / "lanes" / "LANES.md")) == 1 - assert rows(box.wip / "lanes" / "LANES.md") == [], "W's working tree took no write" + assert_W_moved_only_by_fast_forward(box, before) return box, checkout def test_a_new_lanes_second_start_reads_its_row_in_its_own_checkout(tmp_path, short_root): box, checkout = started(tmp_path, short_root) + before = W_reflog(box) second = box.start(window_name=LANE) assert second.returncode == 0, second.stderr assert "ALREADY has a row" not in second.stderr, second.stderr assert len(rows(checkout / "lanes" / "LANES.md")) == 1, "one row, and no second one added" published = box.git("--git-dir", str(box.root / "origin.git"), "show", "main:lanes/LANES.md") assert sum(1 for r in published.splitlines() if r.startswith(f"| `{LANE}`")) == 1 - assert rows(box.wip / "lanes" / "LANES.md") == [], "and W's working tree still took none" + assert_W_moved_only_by_fast_forward(box, before) def test_lane_end_finds_the_row_its_lane_start_wrote(tmp_path, short_root): @@ -80,7 +102,7 @@ def test_lane_end_finds_the_row_its_lane_start_wrote(tmp_path, short_root): capture_output=True, text=True, timeout=120, check=False) assert "has no row for lane" not in end.stderr, end.stderr assert end.returncode == 0, (end.stdout, end.stderr) - assert rows(box.wip / "lanes" / "LANES.md") == [] + assert box.git("-C", str(box.wip), "status", "--porcelain") == "", "W took no write" def test_a_dry_run_start_makes_no_checkout(tmp_path, short_root): @@ -132,8 +154,9 @@ def test_without_the_opt_in_a_new_lane_starts_through_W(tmp_path, short_root): def test_a_checkout_that_cannot_be_made_is_said_and_the_start_goes_on(tmp_path, short_root): """SHOULD 2: where the lane's checkout cannot be cloned, the start says so - and reads W, as it does for a dry run with none: a register act it can do - without never refuses a launch (Amendment 8(d)).""" + and goes on: a register act it can do without never refuses a launch + (Amendment 8(d)). Since slice 2b the row is W's read either way, so only + the writes are touched.""" box = Sandbox(tmp_path) checkout = default_resolution(box, route=False) lane_dir = _checkout_of_length(short_root, None) @@ -150,7 +173,7 @@ def test_a_checkout_that_cannot_be_made_is_said_and_the_start_goes_on(tmp_path, box.env.update(LANES_ROUTE_WRITES="1", LANES_GIT=str(wrapper)) second = box.start(window_name=LANE) assert "own register checkout could not be made" in second.stderr, second.stderr - assert "so this command reads W's register" in second.stderr + assert "ALREADY has a row" not in second.stderr, second.stderr assert not checkout.exists() assert second.returncode == 0, second.stderr @@ -263,7 +286,7 @@ def test_a_held_creation_lock_is_said_and_the_start_goes_on(tmp_path, short_root second = box.start(window_name=LANE, timeout=SLOW) assert "own register checkout could not be made" in second.stderr, second.stderr assert "exited 4" in second.stderr, second.stderr - assert "so this command reads W's register" in second.stderr + assert "ALREADY has a row" not in second.stderr, second.stderr assert not checkout.exists() assert second.returncode == 0, second.stderr diff --git a/tests/test_lane_helpers.sh b/tests/test_lane_helpers.sh index b39e49b..07d20cb 100755 --- a/tests/test_lane_helpers.sh +++ b/tests/test_lane_helpers.sh @@ -3186,7 +3186,7 @@ run "$START" --dry-run --dir "$HOME/projects/repoA" repoZF-1 is "a row this checkout has and origin/main does not still exits 0" "$rc" 0 has "…and says there is no published cell to read" "$err" "the register on origin/main has none" has "…naming the id nothing outside this checkout can see" "$err" "which nothing else can see: $ZF1" -has "…and the act that publishes it" "$err" "push the register from here" +has "…and the act that publishes it, never a bare push (Amendment 23 S3)" "$err" "publish the register from here, then re-run: LANES_LANE=repoZF-1 " has "…taking a NEW session rather than resuming an unpublished id" "$err" "PLAN exec claude --name repoZF-1" hasnt "…which is the whole of the rule: no decision comes off the working tree" "$err" "--resume $ZF1" git -C "$WIP" checkout -q -- lanes/LANES.md @@ -3702,24 +3702,20 @@ is "…and one set of records that agree: the row's newest stamp names it too unset FAKE_TMUX_WINDOW rm -f "$sessions_dir/live-dw.json" -# ---- RV-B2: the handoff stamp's DEGRADED pushes, and which recovery each is +# ---- RV-B2, after Amendment 23's slice 2b: the stamp's DEGRADED pushes # -# F-B7's whole deliverable is a SENTENCE — `hs_carry_note` telling the reader -# whether anything on this workstation will ever push the commit `lane-start` -# just made — and at `77a2f49` not one assertion in this suite read either -# branch of it (`grep -c "register's own checkout" test_lane_helpers.sh` -> 0). -# An untested sentence is what drifts, and this one is the difference between a -# stamp that lands by itself and a stamp nobody will ever see again. +# The stamp is ONE `lanes-edit.sh write-handoff --stamp` now, through the +# register's checkout and under its lock (S3, S4), and `lane-start` runs no git +# of its own: no pull, no push, and no `hs_carry_note`. F-B7's sentence said +# which later write would carry a stranded stamp out; under S3 none does, and +# every later write refuses over it, so there is no sentence left to choose. +# The reader is told the helper's own words, the commit kept and named, and +# lane-start's note naming the publish act. The launch still exits 0. # -# THE REJECTION IS SCOPED BY THE COMMIT, NOT BY THE DIRECTORY. In the first case -# the handoff repo IS the register's checkout, so a blanket refusal of `push -C -# $WIP` would fail `set-row-state` instead — and `lane-start` runs under -# `set -e`, so it would never reach the handoff block the case is about. -# Amendment 8(d) gives the stamp commit the subject `handoff(@): …`, -# which no register write carries, and the push follows its own commit -# immediately: HEAD is therefore an exact discriminator. `commit_push` commits -# before it pulls or pushes for the same reason, so the register's own calls are -# always looking at a register commit when this fake reads HEAD. +# THE REJECTION IS SCOPED BY THE COMMIT, NOT BY THE DIRECTORY, as it was: the +# stamp's commit is the only one whose subject is `handoff(@): …`, +# and `commit_publish` pushes straight after it commits, so HEAD is an exact +# discriminator. `LANES_GIT` is the binary `lanes-edit.sh`'s network calls use. cat > "$SANDBOX/rejectgit" <<'FAKE' #!/usr/bin/env bash # Refuses $RJ_VERB (`push` or `pull`) for the handoff stamp's commit ONLY; @@ -3739,14 +3735,8 @@ exec git "$@" FAKE chmod +x "$SANDBOX/rejectgit" -# The paths as GIT spells them, which is what `lane-start` prints: `hs_repo` is -# `rev-parse --show-toplevel` of the handoff's real path, so comparing against -# the shell's own `$WIP`/`$WS2` would compare two spellings of one directory. -WIP_REAL="$(git -C "$WIP" rev-parse --show-toplevel)" - -# (a) THE HANDOFF IS IN THE REGISTER'S OWN CHECKOUT. The next `lanes-edit.sh` -# write out of it pushes the branch, and the orphaned commit rides out with -# it — so the note may promise the carry, and does. +# (a) A REFUSED PUSH WITH ORIGIN UNMOVED is no lost race, so nothing is +# replayed: the stamp's commit is kept on HEAD and named (S4, exit 6). CN_A="cccc0001-1111-4000-8000-cccc00011111" printf '{"type":"custom-title","customTitle":"repoCN-1","sessionId":"%s"}\n' "$CN_A" > "$tdir/$CN_A.jsonl" mkdir -p "$WIP/handoffs/repoCN" @@ -3761,26 +3751,22 @@ run env LANES_GIT="$SANDBOX/rejectgit" RJ_VERB=push "$START" --dir "$HOME/projec is "a handoff stamp whose PUSH is rejected still exits 0 — the record is not the licence" "$rc" 0 has "…the stamp really written and committed all the same" "$(cat "$WIP/handoffs/repoCN/a.md")" "RESUMED by $CN_A (lane repoCN-1) at " has "…in its own commit, which is the one nothing pushed" "$(git -C "$WIP" log --format=%s -n1)" "handoff(repoCN-1@" -has "…and the reader told what state that leaves, in the branch's own words" "$err" "the push was rejected — the stamp is a local commit here and other workstations cannot see it yet." -has "…told to LOOK before pushing again, never to push blind" "$err" "git -C $WIP_REAL fetch origin main && git -C $WIP_REAL log --oneline origin/main -3" -has "…the re-push conditional on what that look finds" "$err" "git -C $WIP_REAL push origin main # only if it is not there" -has "…and F-B7's sentence for a handoff INSIDE the register's checkout, verbatim" "$err" "(this handoff is in the register's own checkout, so the next lanes-edit.sh write out of it pushes the branch and carries this commit with it.)" -hasnt "…never the other branch's, which would be the wrong promise here" "$err" "no lanes-edit.sh write will ever push it" -# AMENDMENT 23 S3 (#198, slice 1b): this stamp is now a commit origin lacks, -# and every lanes-edit.sh write out of this checkout REFUSES over it instead of -# carrying it out (lane-start's own note still promises the carry; it is slice -# 2b's to reword). So the fixture publishes it here, as its owner would, and -# the sections below start from a checkout with nothing unpublished. +has "…the helper saying why nothing was replayed, and that the commit is kept" "$err" "refused with origin/main unmoved, so this is no lost race and a replay would meet the same refusal; the commit is local and kept on HEAD" +has "…and lane-start naming the publish act, once those words are answered" "$err" "the commit is KEPT on HEAD of that checkout, not pushed" +hasnt "…never promising that a later write carries it: S3 refuses every one" "$err" "carries this commit with it" +# The fixture publishes it, as its owner would, and the sections below start +# from a checkout with nothing unpublished (S3). git -C "$WIP" push -q origin main is "…the stranded stamp is published before any later write meets it (S3)" "$(git -C "$WIP" fetch -q origin && git -C "$WIP" rev-list --count refs/remotes/origin/main..HEAD)" 0 -# (b) THE HANDOFF IS RESOLVED THROUGH `LANES_WORKSPACE_ROOT`, into a checkout no -# `lanes-edit.sh` write ever goes out of. Nothing will carry this commit, -# and promising a carry that will not happen is worse than printing the -# command — so the note names the checkout and says the command is the whole -# of the recovery. Its own bare origin, because the point is that this is a -# SECOND repository: `hs_resolve`'s case (b) must miss before case (c) hits, -# which is why the cell names a file `$WIP` does not have. +# (b) A HANDOFF ONLY ANOTHER CHECKOUT HOLDS. `hs_resolve`'s case (c) found it +# through `LANES_WORKSPACE_ROOT` and stamped it there, in a checkout no +# write ever pushes from. Since slice 2b the stamp goes only where the +# row's cell resolves in the checkout this lane's writes go through +# (E15), so here it is NOT written: nothing is left in a checkout nothing +# publishes, and the line is printed for the session's own first act. The +# cell names a file `$WIP` does not have, in a second repository with its +# own bare origin. WS2_ORIGIN="$SANDBOX/ws2-origin.git"; WS2="$SANDBOX/ws2" git init -q --bare -b main "$WS2_ORIGIN" git clone -q "$WS2_ORIGIN" "$WS2" 2>/dev/null @@ -3799,23 +3785,24 @@ is "the cell names a handoff the register's checkout does NOT hold" "$([ -e "$ run env LANES_GIT="$SANDBOX/rejectgit" RJ_VERB=push LANES_FILE="$LANES" LANES_REPO="$WIP" \ LANES_WORKSPACE_ROOT="$WS2" "$START" --dir "$HOME/projects/repoA" repoCN-2 --no-launch -is "…and a rejected push on THAT checkout exits 0 too" "$rc" 0 -has "…the stamp written into the second checkout, not the register's" "$(cat "$WS2/handoffs/repoCN/b.md")" "RESUMED by $CN_B (lane repoCN-2) at " +is "…and a handoff the register's checkout does not hold still exits 0" "$rc" 0 +hasnt "…the second checkout's copy NOT stamped: a handoff is written only where the lane's writes go" "$(cat "$WS2/handoffs/repoCN/b.md")" "RESUMED by" +is "…nor committed there" "$(git -C "$WS2" log --format=%s -n1)" "handoff fixture for repoCN-2" is "…which the register's checkout never gained a copy of" "$([ -e "$WIP/handoffs/repoCN/b.md" ] && printf present || printf absent)" absent -has "…and F-B7's OTHER sentence, naming the checkout nothing will ever push" "$err" "(this handoff is NOT in the register's checkout ($WS2_REAL), so no lanes-edit.sh write will ever push it: the command above is the whole of the recovery.)" +has "…the run saying the stamp was not written" "$err" "Rule 3's stamp was NOT written" +has "…and printing the line for the session to write as its first act" "$err" "RESUMED by $CN_B (lane repoCN-2) at " hasnt "…never promising a carry that no write will perform" "$err" "carries this commit with it" -# ---- RV-B3: an ABORTED PULL is not proof the commit stayed here +# ---- RV-B3, after slice 2b: THE STAMP NO LONGER PULLS # # The case, live on 2026-09-12T23:04Z: this lane's handoff commit hit a # `pull --rebase` abort on a peer lane's untracked log file, and the peer's own -# helper then rebased and pushed — carrying this lane's local commit `59a2576` -# out to origin UNDERNEATH its own `f053245`. The branch's recovery was a bare -# re-push, so a reader who ran it at that moment would have landed the same Rule -# 3 stamp twice, in the one document this protocol never rewrites. The -# push-rejected branch has always told the reader to look first; this one now -# does too, and the test pins the ORDER as well as the text, because a look -# printed after the push it is meant to gate is not a look. +# helper then rebased and pushed, carrying this lane's local commit out to +# origin underneath its own. That pull is gone: S3 fetches and fast-forwards +# the checkout under its lock before the edit, and S4 replays the edit where +# the push lost a race. So a git that refuses every `pull` while the stamp's +# commit is HEAD changes nothing at all: the stamp is committed and pushed, and +# no pull is run or printed. CN_C="cccc0003-3333-4000-8000-cccc00033333" printf '{"type":"custom-title","customTitle":"repoCN-3","sessionId":"%s"}\n' "$CN_C" > "$tdir/$CN_C.jsonl" printf 'Lane: repoCN-3 — single-use resume prompt\n\n# state\n' > "$WIP/handoffs/repoCN/c.md" @@ -3826,24 +3813,12 @@ git -C "$WIP" push -q origin main "$E" add-row "| \`repoCN-3\` | harness \`$CN_C\` | Eagle / test / brett | 2026-09-11T00:00Z | none | handoffs/repoCN/c.md | ACTIVE |" >/dev/null 2>&1 run env LANES_GIT="$SANDBOX/rejectgit" RJ_VERB=pull "$START" --dir "$HOME/projects/repoA" repoCN-3 --no-launch -is "a handoff stamp whose PULL aborts still exits 0" "$rc" 0 -has "…the stamp written and committed here all the same" "$(git -C "$WIP" log --format=%s -n1)" "handoff(repoCN-3@" -has "…the branch saying what happened, not the timeout's words" "$err" "the pull before the push did not apply (a conflict, or no reachable origin) — the rebase is aborted, the checkout is clean, and the stamp is a local commit here that was not pushed." -hasnt "…and never ruling (h)'s line, which is a different failure" "$err" "the pull before the push TIMED OUT" -has "…then the LOOK, because a peer's push may already carry this very commit" "$err" "git -C $WIP_REAL fetch origin main && git -C $WIP_REAL log --oneline origin/main -3 # a peer's push may already carry it" -has "…and the re-push conditional on what it finds" "$err" "git -C $WIP_REAL pull --rebase origin main && git -C $WIP_REAL push origin main # only if it is not there" -cn_look="$(printf '%s\n' "$err" | grep -n -- 'log --oneline origin/main -3' | head -n1 | cut -d: -f1)" -cn_push="$(printf '%s\n' "$err" | grep -n -- 'pull --rebase origin main && ' | head -n1 | cut -d: -f1)" -is "…the look printed BEFORE the re-push it gates, which is the whole of RV-B3" \ - "$([ -n "$cn_look" ] && [ -n "$cn_push" ] && [ "$cn_look" -lt "$cn_push" ] && printf 'look first' || printf "look $cn_look, push $cn_push")" "look first" -has "…and F-B7's carry note under it, the same two branches as the push case" "$err" "(this handoff is in the register's own checkout, so the next lanes-edit.sh write out of it pushes the branch and carries this commit with it.)" -# AMENDMENT 23 S3 (#198, slice 1b): this stamp is now a commit origin lacks, -# and every lanes-edit.sh write out of this checkout REFUSES over it instead of -# carrying it out (lane-start's own note still promises the carry; it is slice -# 2b's to reword). So the fixture publishes it here, as its owner would, and -# the sections below start from a checkout with nothing unpublished. -git -C "$WIP" push -q origin main -is "…the aborted pull's stamp is published before any later write meets it (S3)" "$(git -C "$WIP" fetch -q origin && git -C "$WIP" rev-list --count refs/remotes/origin/main..HEAD)" 0 +is "a launch whose git refuses the stamp's pull still exits 0" "$rc" 0 +has "…the stamp written and committed here" "$(git -C "$WIP" log --format=%s -n1)" "handoff(repoCN-3@" +is "…and PUSHED: there is no pull for it to need" "$(git -C "$WIP" fetch -q origin && git -C "$WIP" rev-list --count refs/remotes/origin/main..HEAD)" 0 +hasnt "…no pull run or printed" "$err" "pull --rebase" +hasnt "…and none of the old branch's words" "$err" "the pull before the push" +has "…the run saying where it went" "$err" "handoff stamped, committed and pushed: " # ------------------------------------------------- lane-start --confirm / --yes # diff --git a/tests/test_repo_hygiene.py b/tests/test_repo_hygiene.py index 32a35e1..4ba2d9b 100644 --- a/tests/test_repo_hygiene.py +++ b/tests/test_repo_hygiene.py @@ -3267,9 +3267,10 @@ def test_the_exit_3_rule_is_green_on_a_rewording_that_keeps_the_meaning( #: AMENDMENT 23 S3-S4: NO LANE WRITER PULLS (#198). A `pull --rebase` in front #: of a push is what stranded a write on a stale base eight times in two days; #: every writer now fetches and fast-forwards before the edit, and a refused -#: push replays the one edit. Asserted for `lanes-edit.sh` in slice 1b; the -#: handoff's writers, which still pull until their slices, join the list then. -_NO_PULL_FILES = ("lanes-edit.sh",) +#: push replays the one edit. Asserted for `lanes-edit.sh` in slice 1b and for +#: `lane-start` in slice 2b, whose Rule 3 stamp is `write-handoff`'s now; the +#: handoff's other writers, which still pull until their slices, join it then. +_NO_PULL_FILES = ("lanes-edit.sh", "lane-start") #: A `pull` that rebases: `--rebase`, `-r` or `--rebase=…`, on a line that is #: not a comment. _PULL_REBASE = re.compile(r"\bpull\b[^\n]*?(?:--rebase\b|\s-r\b)") From ce73c92d4951b74fd086d057a01e8acca8a0c06b Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Thu, 22 Oct 2026 17:40:46 +0000 Subject: [PATCH 02/19] lane-start reads each restart-intent field to the end of the answer, so a launch no longer dies with 141 when an early-exiting awk closes the pipe under printf (#198, slice 2b) CI run 39806108007 at f1b19e9: test_restart_missing_checksum_is_not_launch_authority exited 141 (SIGPIPE) in `tests` and passed in `tests-no-submodule`. Its stderr ends at "its record refs are", just before the restart-intent read `eri_state`/`eri_op`, read by `printf ... | awk '... { print $2; exit }'` under `set -o pipefail`. bash's printf writes that answer a line at a time, so an awk that has found its line and exited can close the pipe under a writer that is not done; the writer's SIGPIPE is the pipeline's status and `set -e` ends the launch with no word said (openRepoTools#281's class). `intent_field ` reads to the end and keeps the FIRST value, as the early exit did. It now reads `eri_state`, `eri_op` and the twelve `ri_*` fields of the restart reads. The other early-stopping pipes of #281's class (`lane-start` :426, :1336, :2254, :3783, :3789 and lane-handoff's restart reads) are left for #281. New test_restart_intent_answer_larger_than_a_pipe_is_read_to_its_end [missing-checksum|owned-launch]: a lanes-edit.sh wrapper pads restart-intent's answer to about 4.6 MB, so an early-stopping reader is certain to close the pipe before its writer is done. It is red at f1b19e9 (both rc 141, with stderr ending where CI's did), red again with only the `ri_*` reads reverted (141 after "binding: unchanged"), and green with this change. Refs #198 Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 84633dfa60e8318ffa2c06627b85db2ad7b809b0) --- lane-start | 43 +++++++++----- tests/test_supervised_restart_regressions.py | 59 ++++++++++++++++++++ 2 files changed, 88 insertions(+), 14 deletions(-) diff --git a/lane-start b/lane-start index 386bea2..a75ba88 100755 --- a/lane-start +++ b/lane-start @@ -1568,6 +1568,20 @@ esac step "this window is $this_window, named '${this_window_name:-?}'" step " its record refs are $record_window" +# A RESTART-INTENT ANSWER IS READ TO ITS END (slice 2b; CI run 39806108007 at +# f1b19e9, openRepoTools#281's class). Each field used to be read with +# `printf … | awk '… { print $2; exit }'`, and under `set -o pipefail` that is +# a launch that can die with 141 and no word said: bash's `printf` writes the +# answer a line at a time, an `awk` that has found its line and exited closes +# the pipe under a writer that is not done, the writer's SIGPIPE becomes the +# pipeline's status, and `set -e` exits on it — at a rate the scheduler +# decides. So the answer is captured once (`x="$(…)"`), and each field is read +# by an `awk` that reads to the end and keeps the FIRST value, as the early +# exit did. Nothing here pipes into a reader that stops. +intent_field() { # + printf '%s\n' "$1" | awk -F'\t' -v k="$2" '$1 == k && !f { f = 1; v = $2 } END { if (f) print v }' +} + # Active restarts refuse unowned callers before any binding repair or rename. # Detailed prompt and launch-fact validation follows before transcript writes. preparation_failure_identity="" @@ -1598,8 +1612,8 @@ eri_rc=0 eri_out="$(LANES_NO_FETCH=1 "$LANES_EDIT" restart-intent "$LANE" --dir "$DIR" 2>/dev/null)" || eri_rc=$? case "$eri_rc" in 0) - eri_state="$(printf '%s\n' "$eri_out" | awk -F'\t' '$1=="state" {print $2; exit}')" - eri_op="$(printf '%s\n' "$eri_out" | awk -F'\t' '$1=="operation" {print $2; exit}')" + eri_state="$(intent_field "$eri_out" state)" + eri_op="$(intent_field "$eri_out" operation)" case "$eri_state" in preparing) die "this restart is still preparing its preservation records. Nothing was changed." 2 ;; failed) preparation_failure_resume_check "$eri_out" ;; @@ -3270,18 +3284,19 @@ ri_state=""; ri_op=""; ri_gen=""; ri_mode=""; ri_dir=""; ri_hf=""; ri_dig=""; ri ri_out="$(LANES_NO_FETCH=1 "$LANES_EDIT" restart-intent "$LANE" --dir "$DIR" 2>/dev/null)" || ri_rc=$? case "$ri_rc" in 0) - ri_state="$(printf '%s\n' "$ri_out" | awk -F'\t' '$1 == "state" { print $2; exit }')" - ri_op="$(printf '%s\n' "$ri_out" | awk -F'\t' '$1 == "operation" { print $2; exit }')" - ri_gen="$(printf '%s\n' "$ri_out" | awk -F'\t' '$1 == "generation" { print $2; exit }')" - ri_mode="$(printf '%s\n' "$ri_out" | awk -F'\t' '$1 == "mode" { print $2; exit }')" - ri_dir="$(printf '%s\n' "$ri_out" | awk -F'\t' '$1 == "dir" { print $2; exit }')" - ri_hf="$(printf '%s\n' "$ri_out" | awk -F'\t' '$1 == "handoff" { print $2; exit }')" - ri_dig="$(printf '%s\n' "$ri_out" | awk -F'\t' '$1 == "digest" { print $2; exit }')" - ri_pane="$(printf '%s\n' "$ri_out" | awk -F'\t' '$1 == "pane" {print $2; exit}')" - ri_attempt="$(printf '%s\n' "$ri_out" | awk -F'\t' '$1 == "attempt" {print $2; exit}')" - ri_prepared="$(printf '%s\n' "$ri_out" | awk -F'\t' '$1 == "prepared_digest" {print $2; exit}')" - ri_agent="$(printf '%s\n' "$ri_out" | awk -F'\t' '$1 == "agent" {print $2; exit}')" - ri_profile="$(printf '%s\n' "$ri_out" | awk -F'\t' '$1 == "profile" {print $2; exit}')" ;; + # Read to the end, never by a reader that stops (`intent_field`, above). + ri_state="$(intent_field "$ri_out" state)" + ri_op="$(intent_field "$ri_out" operation)" + ri_gen="$(intent_field "$ri_out" generation)" + ri_mode="$(intent_field "$ri_out" mode)" + ri_dir="$(intent_field "$ri_out" dir)" + ri_hf="$(intent_field "$ri_out" handoff)" + ri_dig="$(intent_field "$ri_out" digest)" + ri_pane="$(intent_field "$ri_out" pane)" + ri_attempt="$(intent_field "$ri_out" attempt)" + ri_prepared="$(intent_field "$ri_out" prepared_digest)" + ri_agent="$(intent_field "$ri_out" agent)" + ri_profile="$(intent_field "$ri_out" profile)" ;; # 8 IS AN ANSWER — this lane has no restart in flight, which is every # ordinary launch — and 2 IS A HELPER THAT PREDATES THIS READ: an installed # `lanes-edit.sh` from before openRepoTools#94 answers 2 from its diff --git a/tests/test_supervised_restart_regressions.py b/tests/test_supervised_restart_regressions.py index 0f4cc84..8641ddd 100644 --- a/tests/test_supervised_restart_regressions.py +++ b/tests/test_supervised_restart_regressions.py @@ -744,6 +744,65 @@ def test_restart_missing_checksum_is_not_launch_authority(restart_box): assert box.claude_runs() == "" +# A RESTART-INTENT ANSWER IS READ TO ITS END (slice 2b; CI run 39806108007 at +# f1b19e9, where the case above exited 141 on the runner and passed in the +# next job). `lane-start` read each field of `restart-intent`'s answer with +# `printf … | awk '… { print $2; exit }'` under `set -o pipefail`, and bash's +# `printf` writes that answer a line at a time: an `awk` that has found its +# line and exited closes the pipe under a writer that is not done, the +# writer's SIGPIPE (128 + 13) becomes the pipeline's status, and `set -e` ends +# the launch with 141 and no word said. On a small answer that is a race the +# scheduler decides. Fields no reader asks for, padded past every pipe and +# reader buffer (about 4.6 MB here, against a 64 KiB pipe), leave the early +# reader certain to stop before its writer is done, so these cases are red on +# every run of the old reads, and green on every run of reads that go to the +# end, whatever the timing. +PADDING_LINES = 50000 + + +def pad_restart_intent_answers(box): + real = box.bin / "lanes-edit-real.sh" + (box.bin / "lanes-edit.sh").rename(real) + _write(box.bin / "lanes-edit.sh", f'''#!/usr/bin/env bash +if [ "${{1-}}" = restart-intent ]; then + rc=0 + out="$("$(dirname -- "$0")/lanes-edit-real.sh" "$@")" || rc=$? + printf '%s\\n' "$out" + if [ "$rc" = 0 ]; then + awk 'BEGIN {{ for (i = 0; i < {PADDING_LINES}; i++) printf "zz_padding\\t%080d\\n", i }}' + fi + exit "$rc" +fi +exec "$(dirname -- "$0")/lanes-edit-real.sh" "$@" +''') + + +@pytest.mark.parametrize("kind", ["missing-checksum", "owned-launch"]) +def test_restart_intent_answer_larger_than_a_pipe_is_read_to_its_end(restart_box, kind): + box = restart_box + source = prepare_restart(box) + if kind == "missing-checksum": + assert helper(box, "set-restart-intent", LANE, "pending", "--digest", "none").returncode == 0 + before = source.read_bytes() + pad_restart_intent_answers(box) + padded = helper(box, "restart-intent", LANE) + assert padded.returncode == 0, padded.stderr + assert len(padded.stdout) > 4 * 1024 * 1024 + # The fields the launch reads come first, and the padding after them. + assert "state\tpending" in padded.stdout.splitlines()[:40] + if kind == "missing-checksum": + result = box.start("--operation", "op-test") + assert result.returncode == 2, (result.returncode, result.stderr[-3000:]) + assert "no complete handoff checksum" in result.stderr + assert source.read_bytes() == before + assert box.claude_runs() == "" + else: + result = box.start("--fresh", "--operation", "op-test", TMUX_PANE="%1") + assert result.returncode == 0, (result.returncode, result.stderr[-3000:]) + assert "Preserved task." in box.claude_runs() + assert "RESUMED by" in source.read_text() + + def test_restart_stale_supervisor_cannot_fail_or_retry_newer_operation(restart_box): box = restart_box prepare_restart(box) From b848bd8bd0b5d7c86ea4ae921af68d88c842bc7f Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Fri, 23 Oct 2026 02:48:36 +0000 Subject: [PATCH 03/19] lane-handoff writes the PAUSED top block with one `write-handoff --prepend` through the file `handoff-path` names, so its handoff is committed and pushed under the checkout's lock and never by its own pull and push in W (#198, slice 2b) Amendment 23, act 2's slice 2b, its `lane-handoff` half (lane openRepoTools-3's finding 282 on #307, its second writer): - The handoff file is `lanes-edit.sh handoff-path --create`: the row's cell translated into the checkout this lane's writes go through (its own `-lanes//` under the opt-in, made here where it is not there yet; an explicit LANES_REPO; W where the opt-in is off). 8 is "no handoff named" as before; any other exit is a note in the helper's words. - The publish and the commit block are one `LANES_LANE= write-handoff --prepend --subject "handoff(@): PAUSED, " [--restart 0 preparing none]`, with no LANES_NO_FETCH. The add/commit/`pull --rebase`/push in whatever repository held the file, outside any lock and with git's words sent to /dev/null, is deleted; the helper's own words are printed, with a note for each of its exits (2: S3 refused and the file is written, not committed; 3; 6: kept on HEAD, the publish act named). `handoff_written=1` exactly where the helper's stdout line names the file it wrote, and the pending intent binds that path. - Under `--restart` the preparing bind names that file at its digest now (`none` where it is absent), before the write. - A why's control characters are spelled as spaces in the subject (`subject_text`), which `write-handoff` requires. - `append-line` gains LANES_LANE; a `--restart` respawn line carries LANES_ROUTE_WRITES, so the supervisor's lane-start resolves the checkout the intent bound. The t018 single-repository goldens move exactly where the old commit act printed (`t018-handoff/single-repository-decoys.golden` :34-36 and :108-110, `t018/single-repository.golden` :142-144): `handoff commit: ...` and `handoff pushed to origin/main` become write-handoff's own `committed:` and `pushed origin/main (attempt 1)` lines, its subject carries the why, and lane-handoff says which exit it read. Nothing else in either golden moves; both are re-captured by each module's own `--capture`. Tests: new tests/test_a23_lane_handoff.py (three cases, red at 84633df); hygiene's no-pull rule covers lane-handoff, and the carry rule now reads lane-start and lane-handoff. Refs #198 Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 (cherry picked from commit a1f658ef2f2a3d531636d83227dcecc98993a6a8) --- lane-handoff | 193 ++++++++++-------- .../single-repository-decoys.golden | 10 +- tests/fixtures/t018/single-repository.golden | 5 +- tests/test_a23_lane_handoff.py | 153 ++++++++++++++ tests/test_repo_hygiene.py | 19 +- 5 files changed, 280 insertions(+), 100 deletions(-) create mode 100644 tests/test_a23_lane_handoff.py diff --git a/lane-handoff b/lane-handoff index e1a43ce..ca1fddd 100755 --- a/lane-handoff +++ b/lane-handoff @@ -108,6 +108,10 @@ # 1 a read failed, which is never "nothing to report" (Amendment 7(d)) # # $LANES_EDIT the register's reader/writer (default: beside this file) +# $LANES_ROUTE_WRITES=1 Amendment 23's opt-in: the lane's writes, the +# handoff's included, go through its own register +# checkout `-lanes//`, and a `--restart` +# respawn carries it to the supervisor # $LANES_AGENT this agent's name for the record (default: claude) # $PCLAUDE the launcher (default: lclaude, else pclaude, else claude-profile) # $LANE_HANDOFF_NO_TMUX=1 never touch tmux (the identity fix, --exit, --restart) @@ -2509,28 +2513,35 @@ step "writers polled: $writer_count under ${dir:-}" # empty string before the leading pipe — `lanes-edit.sh`'s own `row_cell` / # `handoff_of_lane` convention. NEVER count back from `NF`: a lane's row carries # every state line to the RIGHT of the handoff column, so `$(NF-2)` lands on the -# empty string and the commit below would `git add ""` — silently, in the one -# step Amendment 6(e) exists for. +# empty string. The cell is read here for the notes below; the FILE is the +# helper's. sread row "'this lane has no row'" register-row "$lane" row_line="$(printf '%s\n' "$row" | grep '^|' | tail -n 1)" handoff_cell="$(printf '%s' "$row_line" | awk -F'|' '{print $7}' | awk '{$1=$1};1')" -handoff_file="" -if [ -n "$handoff_cell" ]; then - case "$handoff_cell" in - '~/'*) [ -f "$HOME/${handoff_cell#'~/'}" ] && handoff_file="$HOME/${handoff_cell#'~/'}" ;; - /*) [ -f "$handoff_cell" ] && handoff_file="$handoff_cell" ;; - *) - # THE WORKSPACE REPOSITORY, THROUGH THE HELPER'S OWN RESOLVER and never - # through a second reading of `workspace.yaml` — Amendment 9(a) is - # explicit that a second way to find one thing is a second answer. - hs_root="$("$LANES_EDIT" workspace-root 2>/dev/null || :)" - for hs_base in "${LANES_REPO:-}" "$hs_root"; do - [ -n "$hs_base" ] || continue - if [ -f "$hs_base/$handoff_cell" ]; then handoff_file="$hs_base/$handoff_cell"; break; fi - done ;; - esac -fi +# THE FILE IS THE ONE `write-handoff` WRITES (Amendment 23, slice 2b): the +# row's cell out of the published register, TRANSLATED into the checkout this +# lane's writes go through — its own `-lanes//` under the opt-in, +# made here where it is not there yet; an explicit `LANES_REPO` in place; W +# where the opt-in is off — by `lanes-edit.sh handoff-path`, the one +# implementation of that answer. It used to be resolved here against +# `LANES_REPO` and then W's `workspace-root`, which under the opt-in is the +# readers' copy no tool writes (S5), so the block landed in W and was +# committed there. Whether the file is THERE is not asked here: the write's own +# S3 fast-forwards a checkout that is behind first, and its 8 is the answer +# for a file that is not. 0 the path · 8 no row, or a row that names no +# handoff · anything else a read or a creation that failed, in the helper's +# own words above. +handoff_file=""; hp_rc=0 +handoff_file="$("$LANES_EDIT" handoff-path "$lane" --create)" || hp_rc=$? +case "$hp_rc" in +0) : ;; +8) handoff_file="" ;; +*) + handoff_file="" + note "where lane $lane's handoff is written could not be read (\`$LANES_EDIT handoff-path $lane --create\` exited $hp_rc; its words are above)" ;; +esac +case "$handoff_file" in /*) : ;; *) handoff_file="" ;; esac # THE FRESH RULE 3 TOP BLOCK. It is PREPENDED and the file is never rewritten: # a handoff is the one document in this protocol whose whole purpose is to be @@ -2602,6 +2613,38 @@ TB_ROWS fi } +# THE SUBJECT IS ONE LINE WITH NO CONTROL CHARACTER, which `write-handoff` +# requires of it: it refuses one (64) and writes nothing. The why is one line +# already (the door at the arguments refuses a newline and a carriage return); +# a tab, or any other control character, is spelled as a space here, so a why +# the old `git commit -m` took still refreshes the handoff. +subject_text() { # + local LC_ALL=C + printf '%s' "${1//[[:cntrl:]]/ }" +} + +# ONE `lanes-edit.sh write-handoff --prepend` (Amendment 23, slice 2b). Under +# the lock of the checkout above, it fast-forwards that checkout (S3), reads +# the file there, puts this block, a `---` line and a blank line in front of it +# and PROVES the old file survives byte for byte, replaces it atomically under +# a restart's fences, commits it by pathspec with the `Lane:` trailer and +# pushes it (S4), replaying the block on origin's new tip where a peer won the +# race. Its one stdout line is the file it wrote and that file's digest. The +# commit used to be this command's own: made in whatever repository held the +# file — W, under the opt-in — after the publish, outside any lock, with a +# rebasing pull and every word git said sent to /dev/null (lane +# openRepoTools-3's finding 282, the second of three such writers). +# +# THE FLAG FOLLOWS THE WRITE AND NEVER THE INTENTION (Copilot round 1 on +# openRepoTools#47). `handoff_written=1` exactly where the helper's stdout +# line says it wrote the file, committed or not: the gates below ask whether +# the next session's first prompt is THIS act's block, and a block written and +# not committed is that (R-A11-11: a swap is never left unwritten; S3's +# refusal writes the file and commits nothing). A file that is read-only or on +# a full filesystem, a refused S5, scope or proof, or a restart fence prints +# no line — and `handoff_written=1` set regardless is what would let +# `--restart` pass its gate and respawn the pane over a STALE top block, which +# is the one document the new session is started with. handoff_written=0 if [ -z "$handoff_file" ]; then note "the row names no handoff this command could find${handoff_cell:+ ('$handoff_cell')}, so the top block was NOT written. The record below is still written; put this block at the head of the lane's handoff by hand:" @@ -2610,81 +2653,45 @@ else hs_tmpd="$(mktemp -d 2>/dev/null || printf '')" if [ -z "$hs_tmpd" ]; then note "could not create a temporary directory, so the handoff was not refreshed: $handoff_file" + elif ! top_block > "$hs_tmpd/block"; then + rm -rf -- "$hs_tmpd" + note "the new top block could not be put in a temporary file, so the handoff was not refreshed: $handoff_file. It is printed below; put it at the head of that file by hand:" + top_block >&2 else - hs_prior_digest="$(handoff_digest "$handoff_file")" - { top_block - printf -- '---\n\n' - cat -- "$handoff_file" - } > "$hs_tmpd/out" - # PROVED BEFORE IT LANDS: taking the new block back out again must give - # the original file byte for byte. That is also what catches a file whose - # last line has no newline. - hs_added="$(( $(grep -c '' "$hs_tmpd/out") - $(grep -c '' "$handoff_file") ))" - if tail -n +$((hs_added + 1)) -- "$hs_tmpd/out" | cmp -s - "$handoff_file"; then - # THE FLAG FOLLOWS THE WRITE AND NEVER THE INTENTION (Copilot round 1 on - # openRepoTools#47). A handoff file that is read-only, or on a filesystem - # that is full, fails HERE — and `handoff_written=1` set regardless is - # what lets `--restart` pass its gate and respawn the pane over a STALE - # top block, which is the one document the new session is started with. - hs_publish_args=(publish-handoff "$lane" "$handoff_file" "$hs_tmpd/out" --expect-digest "$hs_prior_digest") - if [ "$do_restart" = 1 ]; then - # Bind preservation publication to this operation before replacing - # any canonical bytes; the helper verifies file identity under lock. - LANES_NO_FETCH=1 "$LANES_EDIT" set-restart-intent "$lane" preparing \ - --expect preparing --expect-operation "$operation" --expect-generation "$ri_gen" --expect-attempt 0 \ - --handoff "$handoff_file" --digest "$hs_prior_digest" >/dev/null || - { rm -rf -- "$hs_tmpd"; die "the preservation handoff could not be bound to this restart. Nothing was published or respawned." 2; } - hs_publish_args+=(--operation "$operation" --generation "$ri_gen" --attempt 0 --state preparing --transcript none) - fi - if hs_published="$(LANES_NO_FETCH=1 "$LANES_EDIT" "${hs_publish_args[@]}")"; then - handoff_file="$hs_published" - handoff_written=1 - step "handoff refreshed: $handoff_file (+$hs_added lines at the head)" - else - note "the refreshed top block could NOT be written to $handoff_file (the error above is the shell's own), so that file still carries the block it had. It is printed below; put it at the head of that file by hand:" - top_block >&2 - fi - else - note "the splice of the new top block into $handoff_file did not prove out (the old file must survive it byte for byte), so NOTHING was written to it." - top_block >&2 + hs_added="$(( $(grep -c '' "$hs_tmpd/block") + 2 ))" + hs_subject="handoff($lane@${ws:-unknown}): PAUSED, $(subject_text "${why:-handoff}")" + hs_args=(write-handoff "$lane" --prepend "$hs_tmpd/block" --subject "$hs_subject") + if [ "$do_restart" = 1 ]; then + # Bind preservation publication to this operation before replacing + # any canonical bytes: the file the helper writes, at the digest it + # has now (`none` where it is not there yet, and the write's own 8 + # is then the answer). The helper verifies file identity under lock. + LANES_NO_FETCH=1 "$LANES_EDIT" set-restart-intent "$lane" preparing \ + --expect preparing --expect-operation "$operation" --expect-generation "$ri_gen" --expect-attempt 0 \ + --handoff "$handoff_file" --digest "$(handoff_digest "$handoff_file")" >/dev/null || + { rm -rf -- "$hs_tmpd"; die "the preservation handoff could not be bound to this restart. Nothing was published or respawned." 2; } + hs_args+=(--restart "$operation" "$ri_gen" 0 preparing none) fi + hs_out=""; hs_rc=0 + hs_out="$(LANES_LANE="$lane" "$LANES_EDIT" "${hs_args[@]}")" || hs_rc=$? rm -rf -- "$hs_tmpd" - fi -fi - -# The commit is in the LANE'S OWN workspace repository and is not one of the -# writes a missing workstation stops (A11 Addendum 4 ruling 11): it is where the -# gap is NAMED. One commit, an explicit pathspec and the Rule 5 trailer. -if [ "$handoff_written" = 1 ] && [ "${LANES_NO_GIT:-0}" != 1 ]; then - # THE PARENT IS RESOLVED WITH `cd -P`, NOT `readlink -f` (Copilot round 3 on - # openRepoTools#47, the same rule as `lane-start`'s `real_of`). Where - # `readlink -f` is not in the userland 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 a pathspec that "leaves the repository", and the handoff this act - # has just written is left uncommitted. A handoff is reached through the - # estate's `handoffs` symlink on every workstation that has one. - hs_dirreal="$( CDPATH=''; cd -P -- "$(dirname -- "$handoff_file")" 2>/dev/null && pwd -P )" || - hs_dirreal="$(dirname -- "$handoff_file")" - hs_real="$hs_dirreal/$(basename -- "$handoff_file")" - hs_repo="$(git -C "$(dirname -- "$hs_real")" rev-parse --show-toplevel 2>/dev/null || :)" - if [ -n "$hs_repo" ]; then - hs_rel="${hs_real#"$hs_repo"/}" - if git -C "$hs_repo" add -- "$hs_rel" >/dev/null 2>&1 && - git -C "$hs_repo" commit -q -m "handoff($lane@${ws:-unknown}): PAUSED, ${why:-handoff}" -m "Lane: $lane" -- "$hs_rel" >/dev/null 2>&1; then - step "handoff commit: handoff($lane@${ws:-unknown}): PAUSED" - if git -C "$hs_repo" pull --rebase -q origin "${LANES_BRANCH:-main}" >/dev/null 2>&1 && - git -C "$hs_repo" push -q origin "${LANES_BRANCH:-main}" >/dev/null 2>&1; then - step "handoff pushed to origin/${LANES_BRANCH:-main}" - else - git -C "$hs_repo" rebase --abort >/dev/null 2>&1 || : - note "the handoff is committed here and was NOT pushed (a conflict, or no reachable origin). Never force; look first, then push: - git -C $hs_repo fetch origin ${LANES_BRANCH:-main} && git -C $hs_repo log --oneline origin/${LANES_BRANCH:-main} -3 - git -C $hs_repo pull --rebase origin ${LANES_BRANCH:-main} && git -C $hs_repo push origin ${LANES_BRANCH:-main}" - fi + hs_line="$(printf '%s\n' "$hs_out" | awk -F'\t' 'NF >= 2 && $1 != "" { l = $0 } END { print l }')" + if [ -n "$hs_line" ]; then + # THE FILE WRITTEN IS THE FILE BOUND: the pending intent below and + # its read-back name this path, as the helper resolved it. + handoff_file="${hs_line%% *}" + handoff_written=1 + step "handoff refreshed: $handoff_file (+$hs_added lines at the head)" + case "$hs_rc" in + 0) step "handoff written through \`write-handoff\`, which exited 0: its words above say what it committed and pushed" ;; + 2) note "the handoff is written in $handoff_file and NOT committed (\`write-handoff\` exited 2: the checkout refused, and its words above say why and give the commit to make once that is answered). Never re-run this handoff to commit it: a second run puts a second block on top." ;; + 3) note "the handoff is written in $handoff_file and its commit pushed, and origin did not confirm it (\`write-handoff\` exited 3; its words above name the commit): look before writing it again, because a second block on top is a second handoff in the one document this protocol never rewrites." ;; + 6) note "the handoff is written in $handoff_file and committed, and the commit is KEPT on HEAD of that checkout, not pushed (\`write-handoff\` exited 6; its words above name it). No later write publishes it, and each one refuses until it is published (Amendment 23 S3): once those words are answered, LANES_LANE=$lane $LANES_EDIT commit \"\", the publish act." ;; + *) note "the handoff is written in $handoff_file and was NOT published (\`write-handoff\` exited $hs_rc; its words above say what is left)." ;; + esac else - note "the handoff is written and NOT committed — an uncommitted write is captured by the next lane's commit and its attribution is lost (Amendment 4(d)). Commit it yourself: - git -C $hs_repo commit -m \"handoff($lane@${ws:-unknown}): PAUSED\" -m \"Lane: $lane\" -- $hs_rel" + note "the refreshed top block could NOT be written to $handoff_file (\`write-handoff\` exited $hs_rc; its words are above), so that file still carries the block it had. It is printed below; put it at the head of that file by hand:" + top_block >&2 fi fi fi @@ -2887,7 +2894,7 @@ else # and no later line corrects any of them (Amendment 11 clause (e)); a # register line naming it would be the fifth, one file along. if [ -n "$uuid" ]; then - "$LANES_EDIT" append-line "PAUSED — lane $lane, session $uuid@$ws, $UTC, $why_text; handoff refreshed" >/dev/null 2>&1 || + LANES_LANE="$lane" "$LANES_EDIT" append-line "PAUSED — lane $lane, session $uuid@$ws, $UTC, $why_text; handoff refreshed" >/dev/null 2>&1 || note "the LANES.md file-level PAUSED line was not appended (the register may be busy or behind); the object-log line above is the record readers take." else note "no transcript uuid is knowable for this lane, so neither the object log's PAUSED nor the register's file-level line was written — a record naming 'unknown' in that field is a record no restart can read (Amendment 11 clause (e)). The handoff file above is refreshed and is the whole of what this act left behind. Bind the session first: lane-start --no-launch " @@ -3125,6 +3132,12 @@ if [ "$do_restart" = 1 ]; then [ -z "${LANES_HOST:-}" ] || launch_line="LANES_HOST=$(quoted "$LANES_HOST") $launch_line" [ -z "${LANES_OS:-}" ] || launch_line="LANES_OS=$(quoted "$LANES_OS") $launch_line" [ -z "${LANES_CONTAINER:-}" ] || launch_line="LANES_CONTAINER=$(quoted "$LANES_CONTAINER") $launch_line" + # AND THE ROUTING OPT-IN (Amendment 23, slice 2b): the intent above binds + # the file in the checkout THIS command's writes went through, and the + # supervisor's `lane-start` compares the row's file with it. Under the + # opt-in that is the lane's own `-lanes//`; a respawn without it + # would resolve W's copy instead and refuse the restart as two handoffs. + [ -z "${LANES_ROUTE_WRITES:-}" ] || launch_line="LANES_ROUTE_WRITES=$(quoted "$LANES_ROUTE_WRITES") $launch_line" say " respawning $target: $launch_line" if tmux respawn-pane -k -t "$target" "$launch_line" 2>/dev/null; then say " respawned into the supervisor — it launches the fresh session, confirms it came up, and stays in this pane if it did not" diff --git a/tests/fixtures/t018-handoff/single-repository-decoys.golden b/tests/fixtures/t018-handoff/single-repository-decoys.golden index e547835..41ebe51 100644 --- a/tests/fixtures/t018-handoff/single-repository-decoys.golden +++ b/tests/fixtures/t018-handoff/single-repository-decoys.golden @@ -31,9 +31,10 @@ lanes-edit: LANES_NO_FETCH=1 — not fetching; reading origin/main as the ref al lane-handoff: lane=repoD-1 workstation=Eagle lane-handoff: lifecycle: RUNNING -> SWAPPING (generation 2, operation ) lane-handoff: writers polled: 1 under /projects/repoD +lanes-edit: committed: handoff(repoD-1@Eagle): PAUSED, t018 handoff +lanes-edit: pushed origin/main (attempt 1) lane-handoff: handoff refreshed: /projects/wip/handoffs/repoD-1.md (+14 lines at the head) -lane-handoff: handoff commit: handoff(repoD-1@Eagle): PAUSED -lane-handoff: handoff pushed to origin/main +lane-handoff: handoff written through `write-handoff`, which exited 0: its words above say what it committed and pushed lanes-edit: 1 line appended to /projects/wip/lanes/log/repoD-1.md lanes-edit: committed: LOG(repoD-1@Eagle): PAUSED lane:repoD-1 lanes-edit: pushed origin/main (attempt 1) @@ -105,9 +106,10 @@ lanes-edit: LANES_NO_FETCH=1 — not fetching; reading origin/main as the ref al lane-handoff: lane=repoQ-1 workstation=Eagle lane-handoff: lifecycle: RUNNING -> SWAPPING (generation 2, operation ) lane-handoff: writers polled: 1 under /projects/repoQ +lanes-edit: committed: handoff(repoQ-1@Eagle): PAUSED, t018 handoff +lanes-edit: pushed origin/main (attempt 1) lane-handoff: handoff refreshed: /projects/wip/handoffs/repoQ-1.md (+14 lines at the head) -lane-handoff: handoff commit: handoff(repoQ-1@Eagle): PAUSED -lane-handoff: handoff pushed to origin/main +lane-handoff: handoff written through `write-handoff`, which exited 0: its words above say what it committed and pushed lanes-edit: 1 line appended to /projects/wip/lanes/log/repoQ-1.md lanes-edit: committed: LOG(repoQ-1@Eagle): PAUSED lane:repoQ-1 lanes-edit: pushed origin/main (attempt 1) diff --git a/tests/fixtures/t018/single-repository.golden b/tests/fixtures/t018/single-repository.golden index 1d02e80..8235e63 100644 --- a/tests/fixtures/t018/single-repository.golden +++ b/tests/fixtures/t018/single-repository.golden @@ -139,9 +139,10 @@ lanes-edit: LANES_NO_FETCH=1 — not fetching; reading origin/main as the ref al lane-handoff: lane=repoS-1 workstation=Eagle lane-handoff: lifecycle: RUNNING -> SWAPPING (generation 2, operation ) lane-handoff: writers polled: 2 under /projects/repoS +lanes-edit: committed: handoff(repoS-1@Eagle): PAUSED, t018 handoff +lanes-edit: pushed origin/main (attempt 1) lane-handoff: handoff refreshed: /projects/wip/handoffs/repoS-1.md (+15 lines at the head) -lane-handoff: handoff commit: handoff(repoS-1@Eagle): PAUSED -lane-handoff: handoff pushed to origin/main +lane-handoff: handoff written through `write-handoff`, which exited 0: its words above say what it committed and pushed lanes-edit: 1 line appended to /projects/wip/lanes/log/repoS-1.md lanes-edit: committed: LOG(repoS-1@Eagle): PAUSED lane:repoS-1 lanes-edit: pushed origin/main (attempt 1) diff --git a/tests/test_a23_lane_handoff.py b/tests/test_a23_lane_handoff.py new file mode 100644 index 0000000..5475a34 --- /dev/null +++ b/tests/test_a23_lane_handoff.py @@ -0,0 +1,153 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Amendment 23, act 2's slice 2b: `lane-handoff` writes the PAUSED handoff +only through the lane's own register checkout. + +Until this slice `lane-handoff` resolved the row's handoff cell against +`LANES_REPO` and then W (`workspace-root`), published the new top block there +under W's lock, and then added, committed, pulled and pushed it in W with no +lock at all, its output and git's words sent to `/dev/null` (lane +openRepoTools-3's finding 282, its second writer). Under the default +resolution every other write of the lane already went through +`-lanes//` (slice 1a), so the handoff was the one write left in W. + +Now: + * the file is the one `lanes-edit.sh handoff-path --create` names, + in the checkout the lane's writes go through; + * the top block is ONE `write-handoff --prepend` call, under that + checkout's lock and S3/S4: written, committed by pathspec with the + `Lane:` trailer and pushed, and where S3 refuses, written and NOT + committed, with git's and S3's words printed rather than discarded; + * the register's file-level PAUSED line is appended with `LANES_LANE`. + +Each case drives a real `lane-handoff` in `test_lane_start_row_stamp`'s +sandbox with the default resolution (no `LANES_REPO`, no `LANES_FILE`) and the +opt-in `LANES_ROUTE_WRITES=1`, after a real `lane-start` made the lane's row +and checkout. W is brought up to origin first, so the old code finds the +handoff there and its unlocked commit in W is what each case sees at the base. +""" + +from __future__ import annotations + +import shutil +import subprocess +from pathlib import Path + +from conftest import REPO +from test_a23_lane_start import (HANDOFF, _assert_nothing_unpublished, _assert_W_only_fast_forwarded, + _cell, _edit, _origin, _peer_push, _published, _started, _tip) +from test_lane_start_row_stamp import LANE, Sandbox, pytestmark, short_root # noqa: F401 + +WHY = "a test handoff" +SUBJECT = f"handoff({LANE}@Eagle): PAUSED, {WHY}" + + +def _handoff(box: Sandbox, *args: str, env: dict | None = None) -> subprocess.CompletedProcess: + """`lane-handoff --lane repoZ-1 … `, from a shell with no tmux.""" + if not (box.bin / "lane-handoff").exists(): + shutil.copy2(REPO / "lane-handoff", box.bin / "lane-handoff") + (box.bin / "lane-handoff").chmod(0o755) + # A LANE WINDOW'S OWN SEAMS STAY OUT: `WORKBENCHES_CLAUDE_*` names the + # caller's window and checkout, which `lane-handoff` reads where tmux is off. + full = {k: v for k, v in box.env.items() if not k.startswith("WORKBENCHES_")} + full.update(LANE_HANDOFF_NO_TMUX="1", CLAUDE_PROFILE_NAME="test-profile") + full.update(env or {}) + return subprocess.run([str(box.bin / "lane-handoff"), "--lane", LANE, *args, WHY], + env=full, capture_output=True, text=True, timeout=180, check=False) + + +def _ready(tmp_path: Path, short_root: Path) -> tuple[Sandbox, Path, str, list[str]]: + """A started lane whose handoff a peer wrote, with W brought up to origin + (so W has it too, as `lane-start`'s refreshes leave it).""" + box, checkout, _lane_dir, before = _started(tmp_path, short_root) + cell = _cell(box) + _peer_push(box, {cell: HANDOFF}, "the lane's handoff, written elsewhere") + assert _edit(box, "refresh-workspace").returncode == 0 + assert (box.wip / cell).read_text(encoding="utf-8") == HANDOFF + return box, checkout, cell, before + + +# -------------------------------------------------------------------- cases + +def test_handoff_written_and_pushed_from_checkout(tmp_path, short_root): + box, checkout, cell, before = _ready(tmp_path, short_root) + + result = _handoff(box) + assert result.returncode == 0, result.stderr + + published = _published(box, cell) + block, sep, rest = published.partition("\n---\n\n") + assert sep and rest == HANDOFF, published + assert block.startswith(f"Lane: {LANE} (test-profile, session "), block + assert "## RESUME PROMPT — PAUSED " in block and f"({WHY})" in block, block + assert _origin(box, "log", "-1", "--format=%s", "main", "--", cell).strip() == SUBJECT + assert f"Lane: {LANE}" in _origin(box, "log", "-1", "--format=%b", "main", "--", cell) + assert f"handoff refreshed: {checkout}/{cell} " in result.stderr, result.stderr + assert f"committed: {SUBJECT}" in result.stderr, "write-handoff's own words are printed" + + # THE ONE WRITE IN W IS GONE: W moved only by fast-forward, and its copy of + # the handoff took no byte from this act. + _assert_W_only_fast_forwarded(box, before) + assert (box.wip / cell).read_text(encoding="utf-8") in (HANDOFF, published) + assert (checkout / cell).read_text(encoding="utf-8") == published + _assert_nothing_unpublished(box, checkout) + + +def test_S3_refusal_writes_file_no_commit_prints_git_words(tmp_path, short_root): + box, checkout, cell, before = _ready(tmp_path, short_root) + # THE CHECKOUT IS CURRENT FIRST: S3 refuses a foreign staged entry before it + # fast-forwards, so a checkout still behind the peer's handoff commit has no + # file to write into (`write-handoff`'s 8), which is another case. + box.git("-C", str(checkout), "fetch", "-q", "origin", "main") + box.git("-C", str(checkout), "merge", "-q", "--ff-only", "origin/main") + assert (checkout / cell).read_text(encoding="utf-8") == HANDOFF + foreign = checkout / "handoffs" / "repoQ" / "foreign.md" + foreign.parent.mkdir(parents=True, exist_ok=True) + foreign.write_text("a person's file, staged and never committed\n", encoding="utf-8") + box.git("-C", str(checkout), "add", "--", "handoffs/repoQ/foreign.md") + tip = _tip(box) + + result = _handoff(box) + assert result.returncode == 0, result.stderr + + # WRITTEN (R-A11-11: a swap is never left unwritten) and NOT committed. + written = (checkout / cell).read_text(encoding="utf-8") + assert written.startswith(f"Lane: {LANE} (test-profile, session "), written + assert written.endswith("\n---\n\n" + HANDOFF), written + assert box.git("-C", str(checkout), "status", "--porcelain", "--", cell).startswith(" M "), \ + "the handoff is modified in the checkout and not committed" + assert "handoffs/repoQ/foreign.md" in box.git("-C", str(checkout), "diff", "--cached", "--name-only") + assert _tip(box) == tip, "nothing was published, from the lane's checkout or from W" + # THE WORDS ARE PRINTED, NOT SENT TO /dev/null: S3's refusal naming the + # entry, the helper's note, and lane-handoff's own. + assert "the index holds staged entries this write did not make" in result.stderr, result.stderr + assert "handoffs/repoQ/foreign.md" in result.stderr + assert "the handoff is WRITTEN and NOT committed" in result.stderr + assert f"commit -m \"{SUBJECT}\"" in result.stderr, result.stderr + assert "NOT committed" in result.stderr and "write-handoff` exited 2" in result.stderr, result.stderr + + _assert_W_only_fast_forwarded(box, before) + assert (box.wip / cell).read_text(encoding="utf-8") == HANDOFF, "W's copy took no byte" + + +def test_append_line_carries_LANES_LANE(tmp_path, short_root): + box, checkout, cell, _before = _ready(tmp_path, short_root) + calls = box.root / "lanes-edit-calls.log" + wrapper = box.root / "recording-lanes-edit.sh" + wrapper.write_text(f"""#!/usr/bin/env bash +printf '%s LANES_LANE=%s\\n' "${{1-}}" "${{LANES_LANE-}}" >> '{calls}' +exec '{box.bin / "lanes-edit.sh"}' "$@" +""", encoding="utf-8") + wrapper.chmod(0o755) + + result = _handoff(box, env={"LANES_EDIT": str(wrapper)}) + assert result.returncode == 0, result.stderr + + recorded = calls.read_text(encoding="utf-8").splitlines() + for verb in ("append-line", "write-handoff"): + made = [line for line in recorded if line.startswith(verb + " ")] + assert made == [f"{verb} LANES_LANE={LANE}"], (verb, recorded) + register = _published(box, "lanes/LANES.md") + assert f"PAUSED — lane {LANE}, session " in register and f", {WHY} kind unknown; handoff refreshed" in register, \ + register + assert _published(box, cell).endswith("\n---\n\n" + HANDOFF) + _assert_nothing_unpublished(box, checkout) diff --git a/tests/test_repo_hygiene.py b/tests/test_repo_hygiene.py index 4ba2d9b..2e67ac4 100644 --- a/tests/test_repo_hygiene.py +++ b/tests/test_repo_hygiene.py @@ -3267,10 +3267,11 @@ def test_the_exit_3_rule_is_green_on_a_rewording_that_keeps_the_meaning( #: AMENDMENT 23 S3-S4: NO LANE WRITER PULLS (#198). A `pull --rebase` in front #: of a push is what stranded a write on a stale base eight times in two days; #: every writer now fetches and fast-forwards before the edit, and a refused -#: push replays the one edit. Asserted for `lanes-edit.sh` in slice 1b and for -#: `lane-start` in slice 2b, whose Rule 3 stamp is `write-handoff`'s now; the -#: handoff's other writers, which still pull until their slices, join it then. -_NO_PULL_FILES = ("lanes-edit.sh", "lane-start") +#: push replays the one edit. Asserted for `lanes-edit.sh` in slice 1b, and for +#: `lane-start` and `lane-handoff` in slice 2b, whose Rule 3 stamp and PAUSED +#: top block are `write-handoff`'s now; the skill, which still pulls until its +#: slice, joins it then. +_NO_PULL_FILES = ("lanes-edit.sh", "lane-start", "lane-handoff") #: A `pull` that rebases: `--rebase`, `-r` or `--rebase=…`, on a line that is #: not a comment. _PULL_REBASE = re.compile(r"\bpull\b[^\n]*?(?:--rebase\b|\s-r\b)") @@ -3375,6 +3376,16 @@ def test_no_text_promises_the_carry(): assert not found, f"docs/README-lanes.md promises the carry: {found}" +@pytest.mark.parametrize("name", ("lane-start", "lane-handoff")) +def test_no_lane_script_promises_the_carry(name): + """Nor do `lane-start` and `lane-handoff`, whose commits are + `write-handoff`'s since slice 2b: `lane-start`'s carry notes went with + `hs_carry_note`, and `lane-handoff`'s "captured by the next lane's commit" + with its own commit block (#198 §1.10).""" + found = _carry_promises((REPO / name).read_text(encoding="utf-8"), script=True) + assert not found, f"{name} promises the carry: {found}" + + @pytest.mark.parametrize("text,red", [ ("a later write from this checkout may already carry it, so look first", True), ("the next register write from this checkout carries it", True), From 3ce57f67c80f21f82005e8f48c3fdd2646f720cc Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Fri, 23 Oct 2026 03:04:35 +0000 Subject: [PATCH 04/19] The supervised-restart cases run /ctx through the lane's own checkout, and a stamp that could not be published, or that the checkout refused, launches with the bound file at its pre-image digest (#198, slice 2b) Amendment 23, act 2's slice 2b, its tests in tests/test_supervised_restart_regressions.py: - `routed_restart_box` and `prepare_restart_routed`: `restart_box` and `prepare_restart` with the default resolution (no LANES_REPO, no LANES_FILE, `workspace.yaml` naming W, LANES_ROUTE_WRITES=1). The first start makes the lane's checkout and adds the row there; the handoff is committed and pushed from it; the intent binds the file `handoff-path` names. `bind_pending` is the one pending-intent call both share. - `test_ctx_round_trip_through_lane_checkout`: `lane-handoff --restart` writes and pushes the PAUSED block from the checkout and binds that file; its respawn line carries LANES_ROUTE_WRITES=1; a `lane-start` without the opt-in refuses (2) with nothing changed; with it, it launches with the PAUSED block as its first prompt, stamps the checkout's file once and pushes it, and the intent is `starting` at the stamped digest. W moves only by fast-forward. - `test_refused_stamp_with_restart_intent_still_launches` (#198 A2): an entry staged in the checkout makes S3 refuse the stamp; the launch exits 0 with nothing written or published, and the intent goes `pending` -> `starting` at the pre-image digest. - `test_restart_atomic_stamp_failure_preserves_original_for_retry` is rewritten for 5c's `write-handoff`: a stamp that could not be published atomically no longer refuses the launch (exit 0, the bound file unchanged, the intent at its pre-image digest), and the retry stamps it once. All three are red at the base: with `lane-handoff` from 84633df the `--restart` refuses "HANDOFF WAS NOT REFRESHED" (it resolved W's copy); with `lane-start` from e2037b9 the atomic case dies 2, the refused stamp dies 2 "the restart handoff the row names is missing", and the round trip never reaches its launch. Refs #198 Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 37b880617ac5d1791b08841e5095a0e2af368993) --- tests/test_supervised_restart_regressions.py | 167 ++++++++++++++++++- 1 file changed, 159 insertions(+), 8 deletions(-) diff --git a/tests/test_supervised_restart_regressions.py b/tests/test_supervised_restart_regressions.py index 8641ddd..59576ba 100644 --- a/tests/test_supervised_restart_regressions.py +++ b/tests/test_supervised_restart_regressions.py @@ -14,6 +14,7 @@ import pytest from test_lane_start_claude_current import Sandbox, LANE, _write, pytestmark +from test_a23_lane_start_read import W_reflog, assert_W_moved_only_by_fast_forward, default_resolution from conftest import REPO @@ -156,6 +157,16 @@ def test_restart_managed_storage_without_reader_refuses(restart_box): assert (state / "owner.json").read_text() == '{"mode":"managed"}\n' +def bind_pending(box, source): + """The restart intent `pending` for operation op-test, bound to `source`.""" + result = helper(box, "set-restart-intent", LANE, "pending", "--expect", "none", + "--operation", "op-test", "--generation", "7", "--agent", "claude", + "--profile", "test-profile", "--dir", str(box.lane_dir), + "--pane", "testsess:@1.%1", "--handoff", str(source)) + assert result.returncode == 0, result.stderr + return source + + def prepare_restart(box): box.resolver() initial = box.start("--no-launch") @@ -167,12 +178,60 @@ def prepare_restart(box): box.git("-C", str(box.wip), "add", "--", str(source)) box.git("-C", str(box.wip), "commit", "-qm", "seed handoff") box.git("-C", str(box.wip), "push", "-q", "origin", "main") - result = helper(box, "set-restart-intent", LANE, "pending", "--expect", "none", - "--operation", "op-test", "--generation", "7", "--agent", "claude", - "--profile", "test-profile", "--dir", str(box.lane_dir), - "--pane", "testsess:@1.%1", "--handoff", str(source)) - assert result.returncode == 0, result.stderr - return source + return bind_pending(box, source) + + +# AMENDMENT 23, SLICE 2B: THE DEFAULT RESOLUTION. `restart_box` writes through +# `box.wip` by the explicit seam (`LANES_REPO`), so the cases above read what +# the commands wrote there. These take the seam back out: `workspace.yaml` +# names `box.wip` as W, `LANES_ROUTE_WRITES=1` opts into the routing, and every +# write of the lane, the handoff's top block and its Rule 3 stamp included, +# goes through its own register checkout `-lanes//`, while W +# moves only by `refresh-workspace`'s fast-forward. +@pytest.fixture +def routed_restart_box(restart_box: Sandbox) -> Sandbox: + box = restart_box + default_resolution(box) + box.env["CLAUDE_PROFILE_NAME"] = "test-profile" + for where in (("-C", str(box.wip)), ("--git-dir", str(box.origin))): + box.git(*where, "config", "gc.auto", "0") + box.git(*where, "config", "maintenance.auto", "false") + return box + + +def lane_checkout(box) -> Path: + return Path(os.path.realpath(box.wip) + "-lanes") / LANE + + +def intent_record(box) -> dict: + return dict(line.split("\t", 1) for line in helper(box, "restart-intent", LANE).stdout.splitlines()) + + +def published(box, rel) -> bytes: + return subprocess.run(["git", "--git-dir", str(box.origin), "show", f"main:{rel}"], env=box.env, + capture_output=True, check=True).stdout + + +def prepare_restart_routed(box): + """`prepare_restart` through the lane's own checkout: the first start makes + it and adds the row there, the handoff is committed and pushed from it, and + the intent binds the checkout's copy, the file `handoff-path` names, which + `lane-handoff` writes and `lane-start` stamps.""" + box.resolver() + initial = box.start("--no-launch") + assert initial.returncode == 0, initial.stderr + checkout = lane_checkout(box) + assert (checkout / ".git").is_dir(), initial.stderr + path = helper(box, "handoff-path", LANE) + assert path.returncode == 0, path.stderr + source = Path(path.stdout.strip()) + rel = source.relative_to(checkout) + assert str(rel).startswith("handoffs/"), source + _write(source, "# restart handoff\n\nPreserved task.\n---\nHistory.\n", 0o600) + box.git("-C", str(checkout), "add", "--", str(rel)) + box.git("-C", str(checkout), "commit", "-qm", "seed handoff") + box.git("-C", str(checkout), "push", "-q", "origin", "main") + return bind_pending(box, source) def test_restart_opening_prompt_cannot_switch_to_a_different_register_file(restart_box): @@ -396,6 +455,11 @@ def test_restart_reserved_transcript_cannot_be_consumed_twice(restart_box): def test_restart_atomic_stamp_failure_preserves_original_for_retry(restart_box): + """A Rule 3 stamp that could not be published atomically no longer refuses + the launch (Amendment 23, slice 2b): `write-handoff` dies 1 with the bound + file at its bound digest and prints no line, so `lane-start` says so, + launches with the unstamped bound file and records its pre-image digest; + a retry of the same operation stamps it.""" box = restart_box source = prepare_restart(box) before = source.read_bytes() @@ -405,15 +469,102 @@ def test_restart_atomic_stamp_failure_preserves_original_for_retry(restart_box): exec '{real_mv}' "$@" ''') first = box.start("--operation", "op-test") - assert first.returncode == 2, first.stderr + assert first.returncode == 0, first.stderr assert "published atomically" in first.stderr + assert "Rule 3's stamp was NOT written" in first.stderr, first.stderr assert source.read_bytes() == before - assert box.claude_runs() == "" + assert "Preserved task." in box.claude_runs() + record = intent_record(box) + assert record["state"] == "starting", record + assert record["digest"] == hashlib.sha256(before).hexdigest(), record (box.fakebin / "mv").unlink() assert helper(box, "set-restart-intent", LANE, "failed", "--expect", "starting").returncode == 0 assert helper(box, "set-restart-intent", LANE, "starting", "--expect", "failed", "--bump-attempt", "--new-transcript", "none").returncode == 0 retry = box.start("--operation", "op-test") assert retry.returncode == 0, retry.stderr + assert source.read_text().count("RESUMED by") == 1 + + +def test_refused_stamp_with_restart_intent_still_launches(routed_restart_box): + """#198 A2: under a supervised restart, a stamp the lane's checkout refuses + (S3: an entry staged there that this write did not make) is written + nowhere, and the launch goes on with the bound file as it is. The intent + goes `pending` -> `starting` with the PRE-IMAGE digest, the one the first + prompt was read from.""" + box = routed_restart_box + source = prepare_restart_routed(box) + checkout = lane_checkout(box) + rel = str(source.relative_to(checkout)) + before = source.read_bytes() + assert intent_record(box)["state"] == "pending" + foreign = checkout / "handoffs" / "repoQ" / "foreign.md" + _write(foreign, "a person's file, staged and never committed\n", 0o644) + box.git("-C", str(checkout), "add", "--", "handoffs/repoQ/foreign.md") + + result = box.start("--fresh", "--operation", "op-test") + assert result.returncode == 0, (result.stdout, result.stderr) + assert "Rule 3's stamp was NOT written" in result.stderr, result.stderr + assert "STAMP IT AS THIS SESSION'S FIRST ACT" in result.stderr + assert "Preserved task." in box.claude_runs() + assert source.read_bytes() == before, "the refused stamp wrote nothing" + assert published(box, rel) == before, "and published nothing" + assert "handoffs/repoQ/foreign.md" in box.git("-C", str(checkout), "diff", "--cached", "--name-only") + record = intent_record(box) + assert record["state"] == "starting", record + assert record["handoff"] == str(source), record + assert record["digest"] == hashlib.sha256(before).hexdigest(), record + + +def test_ctx_round_trip_through_lane_checkout(routed_restart_box): + """`/ctx` under the opt-in, both halves: `lane-handoff --restart` writes + the PAUSED top block through the lane's checkout and binds THAT file in + the intent, and its respawn line carries `LANES_ROUTE_WRITES`, so the + `lane-start` it runs resolves the same file, launches with that block as + its first prompt and stamps it there. A `lane-start` without the opt-in + resolves W's copy and refuses the restart as two handoffs.""" + box = routed_restart_box + source = prepare_restart_routed(box) + checkout = lane_checkout(box) + rel = str(source.relative_to(checkout)) + assert helper(box, "set-restart-intent", LANE, "ready", "--expect", "pending").returncode == 0 + w_before = W_reflog(box) + + result = supervisor(box, "--restart", "--lane", LANE, "--dir", str(box.lane_dir), "clear") + assert result.returncode == 0, (result.stdout, result.stderr) + respawn = box.tmux_log.read_text() + assert "respawn-pane" in respawn and "LANES_ROUTE_WRITES=1 " in respawn, respawn + paused = source.read_bytes() + assert b"## RESUME PROMPT \xe2\x80\x94 PAUSED " in paused, paused + assert paused.endswith(b"\n---\n\n# restart handoff\n\nPreserved task.\n---\nHistory.\n"), paused + assert published(box, rel) == paused, "the PAUSED block was committed and pushed from the checkout" + record = intent_record(box) + assert record["state"] == "pending" and record["handoff"] == str(source), record + assert record["digest"] == hashlib.sha256(paused).hexdigest(), record + operation = record["operation"] + assert operation != "op-test" + + unrouted = dict(box.env) + unrouted.pop("LANES_ROUTE_WRITES") + pending = intent_path(box).read_bytes() + refused = subprocess.run([str(box.bin / "lane-start"), "repoZ", "1", "--fresh", "--operation", operation], + env=dict(unrouted, FAKE_CC_PATH=str(box.resolved)), cwd=str(box.lane_dir), + capture_output=True, text=True, timeout=120, check=False) + assert refused.returncode == 2, (refused.stdout, refused.stderr) + assert "different handoffs" in refused.stderr or "row names is missing" in refused.stderr, refused.stderr + assert intent_path(box).read_bytes() == pending and source.read_bytes() == paused + assert box.claude_runs() == "" + + launched = box.start("--fresh", "--operation", operation) + assert launched.returncode == 0, (launched.stdout, launched.stderr) + assert "RESUME PROMPT — PAUSED " in box.claude_runs() + stamped = source.read_bytes() + assert stamped.count(b"RESUMED by") == 1 and b"## RESUME PROMPT \xe2\x80\x94 PAUSED " in stamped, stamped + assert stamped.endswith(b"\n---\n\n# restart handoff\n\nPreserved task.\n---\nHistory.\n"), stamped + assert published(box, rel) == stamped, "the stamp was committed and pushed from the checkout" + record = intent_record(box) + assert record["state"] == "starting" and record["handoff"] == str(source), record + assert record["digest"] == hashlib.sha256(stamped).hexdigest(), record + assert_W_moved_only_by_fast_forward(box, w_before) def test_restart_historical_ready_intent_does_not_control_ordinary_resume(restart_box): From dbfbec68a6e514d28bc560c73291e873413f5983 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Fri, 23 Oct 2026 21:47:45 +0000 Subject: [PATCH 05/19] lane-start launches with no first prompt where the lane's own copy of its handoff is not there, naming that file and the command that makes the checkout, and never reads W's copy in its place (#198, slice 2b) Lane openRepoTools-3's review of #316, finding 727 and its question 2. 5d used to fall back to `hs_resolve`, which joined the row's cell to `LANES_REPO` and then to W's `workspace-root`, wherever the file that `handoff-path` names was missing. That read went through W, against Amendment 23 S2 ("never through W"). The copy it found never carried this resume's stamp, and its cell was not fenced the way the writes' cell is, which also kept 723 open. Now the first prompt is read only from the file `handoff-path` names, for every launch. Where that file is not there, the launch goes on with no first prompt, and one note names the missing file and, under the opt-in, `LANES_ROUTE_WRITES=1 lanes-edit.sh lane-checkout --create`. `hs_resolve` and `hs_workspace_root` had no other caller and are deleted. New case: test_no_lane_copy_launches_with_no_first_prompt_never_W_copy (red at 37b8806: the first prompt was W's copy). Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 (cherry picked from commit c8ac6ca71a97024ea7b4ba094b4a348c8fed2d4f) --- lane-start | 85 ++++++++++++------------------------ tests/test_a23_lane_start.py | 43 +++++++++++++++++- 2 files changed, 69 insertions(+), 59 deletions(-) diff --git a/lane-start b/lane-start index a75ba88..f65a316 100755 --- a/lane-start +++ b/lane-start @@ -3238,47 +3238,6 @@ fi # superseded. fresh=0 fresh_why="" -hs_workspace_root() { lanes_workspace_root; } - -# The file the row's handoff cell names. Three spellings, first hit wins, and -# every one of them is a READ: -# (a) a path the cell already spells — absolute, or `~`-rooted, which is -# Amendment 4(e)'s estate spelling reaching the same file through the -# `~/projects//handoffs` symlink; -# (b) `/` — the repository-relative -# spelling every row this script writes carries, and the one the register -# and the handoffs share; -# (c) `/`, for a register reached from -# somewhere else. -# A cell that spells a path of its own is answered by (a) alone: joining an -# absolute path on to a root gives a path nothing has. -# SINCE AMENDMENT 23's SLICE 2B IT IS A FALLBACK AND NEVER A TARGET: the stamp -# is written, the restart's binding is checked and the first prompt is read in -# the file `lanes-edit.sh handoff-path` names, the one this lane's writes go -# to. Only an ordinary launch whose own copy is not there reads the copy this -# finds for its first prompt, and says so (5d). -hs_resolve() { - local cell="$1" cand root - [ -n "$cell" ] || return 1 - case "$cell" in - '~/'*) cand="$HOME/${cell#'~/'}" ;; - /*) cand="$cell" ;; - *) cand="" ;; - esac - if [ -n "$cand" ]; then - [ -f "$cand" ] || return 1 - printf '%s\n' "$cand"; return 0 - fi - if [ -n "${LANES_REPO:-}" ] && [ -f "$LANES_REPO/$cell" ]; then - printf '%s\n' "$LANES_REPO/$cell"; return 0 - fi - root="$(hs_workspace_root 2>/dev/null || :)" - if [ -n "$root" ] && [ -f "$root/$cell" ]; then - printf '%s\n' "$root/$cell"; return 0 - fi - return 1 -} - first_prompt=0 ri_state=""; ri_op=""; ri_gen=""; ri_mode=""; ri_dir=""; ri_hf=""; ri_dig=""; ri_rc=0; ri_expected_digest=""; ri_prompt=""; ri_active=0; ri_attempt=""; ri_prepared=""; ri_agent=""; ri_profile="" ri_out="$(LANES_NO_FETCH=1 "$LANES_EDIT" restart-intent "$LANE" --dir "$DIR" 2>/dev/null)" || ri_rc=$? @@ -4184,7 +4143,7 @@ if [ -n "$ls_cell" ]; then else HS_PATH="" [ "$hsp_rc" = 8 ] || (( dry_run )) || - note "where this lane's handoff is written could not be read (\`$LANES_EDIT handoff-path $LANE\` exited $hsp_rc), so the first prompt below is read from the file the row's cell names" + note "where this lane's handoff is written could not be read (\`$LANES_EDIT handoff-path $LANE\` exited $hsp_rc), and a first prompt is read from that file alone, never from W's copy (5d)" fi fi @@ -4492,21 +4451,18 @@ if (( first_prompt )); then else fp_cell="$handoff" fi - # READS FOLLOW THE WRITE (Amendment 23, slice 2b): the file 5c wrote, in the - # checkout this lane's writes go through, and not W's copy, which nothing - # here brought up to date. Under a supervised restart it is the only file - # there is, because its intent bound it. For any other launch, where that - # file is not there, the copy the row's cell names is read, and said. + # READS FOLLOW THE WRITE, AND NEVER THROUGH W (Amendment 23 S2, slice 2b): + # the file 5c wrote, in the checkout this lane's writes go through. Under a + # supervised restart it is the only file there is, because its intent bound + # it, and so it is for every other launch. Where that file is not there the + # launch goes on with NO first prompt, and says which file is missing and + # what makes it (below). W's copy is never read in its place: it never + # carries this resume's stamp, and the cell it is found by is not fenced as + # the writes' is (lane openRepoTools-3's review of #316, 727 and its + # question 2). fp_file="" - if [ -n "$fp_cell" ]; then - if [ -n "$HS_PATH" ] && [ -f "$HS_PATH" ]; then - fp_file="$HS_PATH" - elif [ "$ri_active" != 1 ] || (( ! fresh )); then - fp_file="$(hs_resolve "$fp_cell" 2>/dev/null || :)" - if [ -n "$fp_file" ] && [ -n "$HS_PATH" ]; then - note "this lane's own copy of its handoff is not at $HS_PATH, so the first prompt is read from $fp_file, which may be behind it" - fi - fi + if [ -n "$fp_cell" ] && [ -n "$HS_PATH" ] && [ -f "$HS_PATH" ]; then + fp_file="$HS_PATH" fi if [ "$ri_active" = 1 ] && (( fresh )); then [ -n "$fp_file" ] && [ "$fp_file" -ef "$ri_hf" ] || @@ -4530,8 +4486,21 @@ if (( first_prompt )); then else # NEVER A REASON TO REFUSE THE LAUNCH, for the same reason a handoff that # cannot be stamped is not: Rule 3's block is the RECORD a session resumes - # from, and a session with no prompt can still read the file itself. - note "the handoff the row names was not found${fp_cell:+ ('$fp_cell')}, so $AGENT is launched with no first prompt. Its first act is to read that file and stamp it (Rule 3)." + # from, and a session with no prompt can still read the file itself. The + # note names the file this lane's writes go to and, under the opt-in, the + # command that makes the lane's checkout it is in. + fp_make="" + [ "$LS_ROUTED" != 1 ] || + fp_make=" Where the lane's own checkout is not there, LANES_ROUTE_WRITES=1 $LANES_EDIT lane-checkout $LANE --create makes it." + if [ -z "$fp_cell" ]; then + note "the row names no handoff, so $AGENT is launched with no first prompt." + elif [ -n "$HS_PATH" ]; then + fp_state="is not at $HS_PATH" + [ -z "$fp_file" ] || fp_state="is empty at $HS_PATH" + note "this lane's handoff '$fp_cell' $fp_state, the file its writes go to, so $AGENT is launched with no first prompt, and W's copy is never read in its place (Amendment 23 S2).${fp_make} Its first act is to read that file and stamp it (Rule 3)." + else + note "where this lane's handoff '$fp_cell' is written could not be read (above), so $AGENT is launched with no first prompt, and W's copy is never read in its place (Amendment 23 S2).${fp_make} \`$LANES_EDIT handoff-path $LANE\` names the file; its first act is to read it and stamp it (Rule 3)." + fi fi fi diff --git a/tests/test_a23_lane_start.py b/tests/test_a23_lane_start.py index c8c126b..08819f2 100644 --- a/tests/test_a23_lane_start.py +++ b/tests/test_a23_lane_start.py @@ -31,7 +31,7 @@ import subprocess from pathlib import Path -from test_a23_lane_start_read import default_resolution, rows +from test_a23_lane_start_read import _no_clone_git, default_resolution, rows from test_lane_start_row_stamp import LANE, Sandbox, _checkout_of_length, pytestmark, short_root # noqa: F401 HANDOFF = "Lane: repoZ-1 — single-use resume prompt\n\n# state\n\nthe lane's own words\n" @@ -299,3 +299,44 @@ def test_publish_failure_no_longer_refuses_a_plain_resume(tmp_path, short_root): assert "Nothing was launched" not in start.stderr assert "RESUMED by " not in _published(box, cell) assert os.path.exists(box.wip / cell) + + +# ------------------------------------------- fix round 1 (#316's review) + +def _first_prompt_note(stderr: str) -> str: + """5d's one line where the launch goes on with no first prompt.""" + found = [line for line in stderr.splitlines() if "launched with no first prompt" in line] + assert len(found) == 1, stderr + return found[0] + + +def test_no_lane_copy_launches_with_no_first_prompt_never_W_copy(tmp_path, short_root): + """727, and question 2 of lane openRepoTools-3's review of #316: where the + lane's own copy of its handoff is not there (here its checkout cannot be + made), the launch goes on with NO first prompt. It names the missing file + and the command that makes the checkout, and it never reads W's copy, + which does not carry this resume's stamp (Amendment 23 S2: reads follow + the writes, never through W).""" + box = Sandbox(tmp_path) + checkout = default_resolution(box, route=False) + _quiet(box, box.wip) + lane_dir = _checkout_of_length(short_root, None) + box.checkout(lane_dir) + first = box.start("--dir", str(lane_dir), window_name="claude") + assert first.returncode == 0, first.stderr + cell = _cell(box) + _peer_push(box, {cell: HANDOFF}, "the lane's handoff, written elsewhere") + box.env["LANES_ROUTE_WRITES"] = "1" + assert _edit(box, "refresh-workspace").returncode == 0 + assert "the lane's own words" in (box.wip / cell).read_text(encoding="utf-8"), "W holds the handoff" + box.env["LANES_GIT"] = str(_no_clone_git(box)) + + start = box.start("--fresh", window_name=LANE) + assert start.returncode == 0, start.stderr + assert "own register checkout could not be made" in start.stderr, start.stderr + assert not checkout.exists() + assert "own words" not in start.stdout, "W's copy is never the first prompt" + assert "first prompt: the top block of" not in start.stderr, start.stderr + note = _first_prompt_note(start.stderr) + assert f"{checkout}/{cell}" in note, note + assert f"lane-checkout {LANE} --create" in note, note From dc50eb5a7e28ef886d959db6f57d3adc14093702 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Fri, 23 Oct 2026 22:00:45 +0000 Subject: [PATCH 06/19] lane-start and lane-handoff say, under LANES_NO_GIT=1, that the handoff is written and nothing is committed, where their exit-0 words claimed a commit and a push (#198, slice 2b) Lane openRepoTools-3's review of #316, findings 721 and 728 and its question 4, and rv2b's SHOULD 1. Under LANES_NO_GIT=1, `write-handoff` writes the file, commits nothing and exits 0. lane-start then printed "handoff stamped, committed and pushed", which was false. lane-handoff printed "its words above say what it committed and pushed". Now both scripts say the file is written and NOT committed under LANES_NO_GIT=1. Every other exit 0 keeps its words byte for byte, so the t018 goldens do not move. The 8(d) and sibling cases wait for slice 2a's 604 and 601 (part 2). New cases, both red at c8ac6ca: test_no_git_stamp_says_written_and_not_committed and test_no_git_handoff_says_written_and_nothing_committed. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 127ac759a7ea1d62998dbed177a3c93d9f96ae4e) --- lane-handoff | 10 +++++++++- lane-start | 9 ++++++++- tests/test_a23_lane_handoff.py | 24 ++++++++++++++++++++++++ tests/test_a23_lane_start.py | 27 +++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/lane-handoff b/lane-handoff index ca1fddd..02f7594 100755 --- a/lane-handoff +++ b/lane-handoff @@ -2683,7 +2683,15 @@ else handoff_written=1 step "handoff refreshed: $handoff_file (+$hs_added lines at the head)" case "$hs_rc" in - 0) step "handoff written through \`write-handoff\`, which exited 0: its words above say what it committed and pushed" ;; + # THE WORDS SAY WHAT WAS DONE (lane openRepoTools-3's review of + # #316, 728 and its question 4): under the harness's + # LANES_NO_GIT=1 the helper writes the file, commits nothing and + # exits 0. + 0) if [ "${LANES_NO_GIT:-0}" = 1 ]; then + step "handoff written through \`write-handoff\`, which exited 0 under LANES_NO_GIT=1: the block is written in $handoff_file and nothing is committed or pushed" + else + step "handoff written through \`write-handoff\`, which exited 0: its words above say what it committed and pushed" + fi ;; 2) note "the handoff is written in $handoff_file and NOT committed (\`write-handoff\` exited 2: the checkout refused, and its words above say why and give the commit to make once that is answered). Never re-run this handoff to commit it: a second run puts a second block on top." ;; 3) note "the handoff is written in $handoff_file and its commit pushed, and origin did not confirm it (\`write-handoff\` exited 3; its words above name the commit): look before writing it again, because a second block on top is a second handoff in the one document this protocol never rewrites." ;; 6) note "the handoff is written in $handoff_file and committed, and the commit is KEPT on HEAD of that checkout, not pushed (\`write-handoff\` exited 6; its words above name it). No later write publishes it, and each one refuses until it is published (Amendment 23 S3): once those words are answered, LANES_LANE=$lane $LANES_EDIT commit \"\", the publish act." ;; diff --git a/lane-start b/lane-start index f65a316..154c617 100755 --- a/lane-start +++ b/lane-start @@ -4405,7 +4405,14 @@ if [ "$lane_verb" = RESUMED ] && [ "$row_count" = 1 ] && (( stamp_ok )); then # helper reserved in the intent before the bytes: 5d reads that file. if [ "$ri_active" = 1 ]; then ri_expected_digest="${hs_line##* }"; fi case "$hs_rc" in - 0) step "handoff stamped, committed and pushed: $hs_written: $hs_stamp" ;; + # THE WORDS SAY WHAT WAS DONE (lane openRepoTools-3's review of #316, + # 721 and its question 4): under the harness's LANES_NO_GIT=1 the + # helper writes the file and commits nothing, and exits 0. + 0) if [ "${LANES_NO_GIT:-0}" = 1 ]; then + note "Rule 3's stamp is written in $hs_written and NOT committed: LANES_NO_GIT=1 commits and pushes nothing (\`write-handoff\` exited 0): $hs_stamp" + else + step "handoff stamped, committed and pushed: $hs_written: $hs_stamp" + fi ;; 3) note "Rule 3's stamp is written in $hs_written and its commit pushed, and origin did not confirm it (\`write-handoff\` exited 3; its words above name the commit): look before writing it again, because a stamp written twice is two resumes in the one document this protocol never rewrites." ;; 6) note "Rule 3's stamp is written in $hs_written and committed, and the commit is KEPT on HEAD of that checkout, not pushed (\`write-handoff\` exited 6; its words above name it). No later write publishes it, and each one refuses until it is published (Amendment 23 S3): once those words are answered, LANES_LANE=$LANE $LANES_EDIT commit \"\", the publish act." ;; *) note "Rule 3's stamp is written in $hs_written and was NOT published (\`write-handoff\` exited $hs_rc; its words above say what is left). The launch is not refused for it." ;; diff --git a/tests/test_a23_lane_handoff.py b/tests/test_a23_lane_handoff.py index 5475a34..22ab34a 100644 --- a/tests/test_a23_lane_handoff.py +++ b/tests/test_a23_lane_handoff.py @@ -151,3 +151,27 @@ def test_append_line_carries_LANES_LANE(tmp_path, short_root): register assert _published(box, cell).endswith("\n---\n\n" + HANDOFF) _assert_nothing_unpublished(box, checkout) + + +def test_no_git_handoff_says_written_and_nothing_committed(tmp_path, short_root): + """728, and question 4 of the review of #316: under `LANES_NO_GIT=1` the + block is written in the lane's checkout and nothing is committed, and the + exit-0 words say so rather than point at a commit and a push.""" + box, checkout, cell, _before = _ready(tmp_path, short_root) + # NOTHING IS FETCHED UNDER LANES_NO_GIT, so the checkout is current first. + box.git("-C", str(checkout), "fetch", "-q", "origin", "main") + box.git("-C", str(checkout), "merge", "-q", "--ff-only", "origin/main") + head = box.git("-C", str(checkout), "rev-parse", "HEAD") + tip = _tip(box) + + result = _handoff(box, env={"LANES_NO_GIT": "1"}) + assert result.returncode == 0, result.stderr + written = (checkout / cell).read_text(encoding="utf-8") + assert written.startswith(f"Lane: {LANE} (test-profile, session "), written + assert written.endswith("\n---\n\n" + HANDOFF), written + assert box.git("-C", str(checkout), "rev-parse", "HEAD") == head, "nothing is committed" + assert _tip(box) == tip, "or pushed" + assert "what it committed and pushed" not in result.stderr, result.stderr + said = [line for line in result.stderr.splitlines() + if "LANES_NO_GIT=1" in line and "nothing is committed" in line] + assert len(said) == 1, result.stderr diff --git a/tests/test_a23_lane_start.py b/tests/test_a23_lane_start.py index 08819f2..55d7653 100644 --- a/tests/test_a23_lane_start.py +++ b/tests/test_a23_lane_start.py @@ -340,3 +340,30 @@ def test_no_lane_copy_launches_with_no_first_prompt_never_W_copy(tmp_path, short note = _first_prompt_note(start.stderr) assert f"{checkout}/{cell}" in note, note assert f"lane-checkout {LANE} --create" in note, note + + +def test_no_git_stamp_says_written_and_not_committed(tmp_path, short_root): + """721, and question 4 of the review of #316 (rv2b's SHOULD 1): under + `LANES_NO_GIT=1` `write-handoff` writes the stamp, commits nothing and + exits 0. The exit-0 words say that, never "committed and pushed".""" + box, checkout, _lane_dir, _before = _started(tmp_path, short_root) + cell = _cell(box) + _peer_push(box, {cell: HANDOFF}, "the lane's handoff, written elsewhere") + # NOTHING IS FETCHED UNDER LANES_NO_GIT (S3 returns at once), so the + # checkout is brought up to the handoff first. + box.git("-C", str(checkout), "fetch", "-q", "origin", "main") + box.git("-C", str(checkout), "merge", "-q", "--ff-only", "origin/main") + head = box.git("-C", str(checkout), "rev-parse", "HEAD") + tip = _tip(box) + box.env["LANES_NO_GIT"] = "1" + + start = box.start("--fresh", window_name=LANE) + assert start.returncode == 0, start.stderr + assert (checkout / cell).read_text(encoding="utf-8").splitlines()[2].startswith("RESUMED by "), \ + "the stamp is written in the lane's checkout" + assert box.git("-C", str(checkout), "rev-parse", "HEAD") == head, "and nothing is committed" + assert _tip(box) == tip, "or pushed" + assert "committed and pushed" not in start.stderr, start.stderr + said = [line for line in start.stderr.splitlines() if "LANES_NO_GIT=1" in line and "NOT committed" in line] + assert len(said) == 1, start.stderr + assert f"{checkout}/{cell}" in said[0], said From 31990c48e6d983589c47a606a32e3c4b9ea96b5d Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Sat, 24 Oct 2026 14:42:18 +0000 Subject: [PATCH 07/19] lane-start says, where the lane's checkout refused a plain launch's stamp, that the first prompt is the checkout's copy and origin's differs, naming both copies and the command that brings the checkout up to date (#198, slice 2b) Lane openRepoTools-3's review of #316, finding 725. Routed, a staged stray in the lane's checkout makes S3 refuse the stamp, and the checkout is not fast-forwarded. Where origin holds a newer PAUSED block from another workstation, 5d delivered the checkout's older block and did not say so. At e2037b9 the newer block was delivered. Now, where 5c wrote nothing on a plain launch, 5d compares the checkout's file with the checkout's own origin/, which the refused write has just fetched (blob ids). Where they differ, one note names both copies, `git -C merge --ff-only origin/` once the refusal is answered, and how to read origin's copy. The launch still goes on with the checkout's copy, the file this lane's writes go to. Under a restart the bound file stays the prompt, by 5d's fences. New case, red at 127ac75: test_refused_stamp_names_a_checkout_copy_behind_origin. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 77e94b921fac0d100eddea6aeac4fe5bc757f5c2) --- lane-start | 22 ++++++++++++++++++++++ tests/test_a23_lane_start.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/lane-start b/lane-start index 154c617..66b527b 100755 --- a/lane-start +++ b/lane-start @@ -4377,6 +4377,7 @@ hs_bound_unchanged() { # the restart's bound file still digests as the restart [ "${ri_expected_digest:-none}" != none ] || return 0 [ -n "$ri_hf" ] && [ "$ri_hf" != none ] && [ "$(handoff_digest_of "$ri_hf")" = "$ri_expected_digest" ] } +hs_none=0 # 1: 5c ran and wrote nothing, and the launch goes on (725, in 5d) if [ "$lane_verb" = RESUMED ] && [ "$row_count" = 1 ] && (( stamp_ok )); then hs_cell="$(cell 7 | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' || :)" hs_stamp="RESUMED by $stamp_who (lane $LANE) at $utc — lane-start on $ws" @@ -4422,6 +4423,7 @@ if [ "$lane_verb" = RESUMED ] && [ "$row_count" = 1 ] && (( stamp_ok )); then elif [ "$ri_active" = 1 ] && ! hs_bound_unchanged; then die "the restart handoff changed before its owned resume stamp. Nothing was launched." 2 else + hs_none=1 note "Rule 3's stamp was NOT written (\`write-handoff\` exited $hs_rc; its words are above), and the launch is not refused for it: the stamp is the record of this resume, not its licence." note " STAMP IT AS THIS SESSION'S FIRST ACT — this line under the handoff's header block:" note " $hs_stamp" @@ -4471,6 +4473,26 @@ if (( first_prompt )); then if [ -n "$fp_cell" ] && [ -n "$HS_PATH" ] && [ -f "$HS_PATH" ]; then fp_file="$HS_PATH" fi + # A REFUSED STAMP LEAVES THE CHECKOUT WHERE IT WAS (lane openRepoTools-3's + # review of #316, 725). S3 fetched origin before it refused, and it did not + # fast-forward, so the file read below may be older than origin's, which + # another workstation may have moved on. A plain launch still goes on with + # this file, the one this lane's writes go to; where the checkout's own + # `origin/` differs, both copies are named, and the command that + # brings the checkout up to date. (Under a restart the bound file is the + # prompt, by the fences below.) + if [ "$hs_none" = 1 ] && [ "$ri_active" != 1 ] && [ -n "$fp_file" ]; then + fp_dir="$(dirname -- "$fp_file")"; fp_base="$(basename -- "$fp_file")" + fp_br="${LANES_BRANCH:-main}" + fp_theirs="$(git -C "$fp_dir" rev-parse -q --verify "refs/remotes/origin/$fp_br:./$fp_base" 2>/dev/null || :)" + fp_ours="$(git -C "$fp_dir" hash-object -- "$fp_base" 2>/dev/null || :)" + if [ -n "$fp_theirs" ] && [ -n "$fp_ours" ] && [ "$fp_theirs" != "$fp_ours" ]; then + fp_top="$(git -C "$fp_dir" rev-parse --show-toplevel 2>/dev/null || :)" + [ -n "$fp_top" ] || fp_top="$fp_dir" + fp_rel="${fp_file#"$fp_top"/}" + note "this lane's handoff in its checkout, $fp_file, is not origin's: origin/$fp_br:$fp_rel, as the refused stamp's fetch left it in $fp_top, differs, so the first prompt below is the checkout's copy and may be older than origin's. Once the refusal above is answered, git -C $fp_top merge --ff-only origin/$fp_br brings the checkout up to date (origin's copy reads with git -C $fp_top show origin/$fp_br:$fp_rel)." + fi + fi if [ "$ri_active" = 1 ] && (( fresh )); then [ -n "$fp_file" ] && [ "$fp_file" -ef "$ri_hf" ] || die "the delivered restart prompt is not the intent-bound handoff. Nothing was launched." 2 diff --git a/tests/test_a23_lane_start.py b/tests/test_a23_lane_start.py index 55d7653..21c6b0c 100644 --- a/tests/test_a23_lane_start.py +++ b/tests/test_a23_lane_start.py @@ -367,3 +367,35 @@ def test_no_git_stamp_says_written_and_not_committed(tmp_path, short_root): said = [line for line in start.stderr.splitlines() if "LANES_NO_GIT=1" in line and "NOT committed" in line] assert len(said) == 1, start.stderr assert f"{checkout}/{cell}" in said[0], said + + +NEWER = "Lane: repoZ-1 — single-use resume prompt\n\n# state\n\nNEWER BLOCK FROM ANOTHER WORKSTATION\n" + + +def test_refused_stamp_names_a_checkout_copy_behind_origin(tmp_path, short_root): + """725 of the review of #316: the lane's checkout refuses a plain launch's + stamp (a stray is staged there) while origin holds a newer PAUSED block + from another workstation. The launch goes on with the checkout's copy, + and says that origin's differs: it names both copies and the command that + brings the checkout up to date.""" + box, checkout, _lane_dir, _before = _started(tmp_path, short_root) + cell = _cell(box) + _peer_push(box, {cell: HANDOFF}, "the lane's handoff") + box.git("-C", str(checkout), "fetch", "-q", "origin", "main") + box.git("-C", str(checkout), "merge", "-q", "--ff-only", "origin/main") + _peer_push(box, {cell: NEWER}, "a newer PAUSED block, from another workstation") + foreign = checkout / "handoffs" / "repoQ" / "foreign.md" + foreign.parent.mkdir(parents=True, exist_ok=True) + foreign.write_text("a person's file, staged and never committed\n", encoding="utf-8") + box.git("-C", str(checkout), "add", "--", "handoffs/repoQ/foreign.md") + + start = box.start("--fresh", window_name=LANE) + assert start.returncode == 0, start.stderr + assert "Rule 3's stamp was NOT written" in start.stderr, start.stderr + said = [line for line in start.stderr.splitlines() if "is not origin's" in line] + assert len(said) == 1, start.stderr + assert f"{checkout}/{cell}" in said[0], said + assert f"origin/main:{cell}" in said[0], said + assert f"git -C {checkout} merge --ff-only origin/main" in said[0], said + assert "own words" in start.stdout, "the first prompt is the checkout's copy, as said" + assert "NEWER BLOCK" not in start.stdout From e97ad3ff5322a86a47b5496336bf84dd5915a91a Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Sun, 8 Nov 2026 14:12:54 +0000 Subject: [PATCH 08/19] lane-start says, under a supervised restart, to run a refused stamp's printed re-run only once the restart is confirmed ready, since that write carries none of the restart's fences and exits 7 while the intent is starting (opensoft/openRepoTools#198, slice 2b) Lane openRepoTools-3's review of opensoft/openRepoTools#316, NIT 726. Where the stamp could not be written, lane-start prints the exact `write-handoff --stamp` re-run. That line carries none of a restart's fences, so run while the intent is `starting` the helper refuses it with 7. Now, under an active restart, a second line says to run it only once the restart is confirmed ready. A plain launch's words are unchanged. `test_refused_stamp_with_restart_intent_still_launches` now asserts the re-run line (`with: LANES_LANE=repoZ-1 `, which pins the battery's survivor LS49) and the new line; red at the parent for the new line. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 --- lane-start | 5 +++++ tests/test_supervised_restart_regressions.py | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/lane-start b/lane-start index 66b527b..cf4be3e 100755 --- a/lane-start +++ b/lane-start @@ -4428,6 +4428,11 @@ if [ "$lane_verb" = RESUMED ] && [ "$row_count" = 1 ] && (( stamp_ok )); then note " STAMP IT AS THIS SESSION'S FIRST ACT — this line under the handoff's header block:" note " $hs_stamp" note " with: $hs_fix" + # THAT RE-RUN CARRIES NONE OF A RESTART'S FENCES (lane openRepoTools-3's + # review of #316, 726): run while the intent is `starting`, the helper + # refuses it as a write without them (7). + [ "$ri_active" != 1 ] || + note " and only once this restart is confirmed ready: while its intent is \`starting\` that write, which carries none of the restart's fences, exits 7." fi fi fi diff --git a/tests/test_supervised_restart_regressions.py b/tests/test_supervised_restart_regressions.py index 59576ba..c516839 100644 --- a/tests/test_supervised_restart_regressions.py +++ b/tests/test_supervised_restart_regressions.py @@ -505,6 +505,10 @@ def test_refused_stamp_with_restart_intent_still_launches(routed_restart_box): assert result.returncode == 0, (result.stdout, result.stderr) assert "Rule 3's stamp was NOT written" in result.stderr, result.stderr assert "STAMP IT AS THIS SESSION'S FIRST ACT" in result.stderr + # 726 of the review of #316: the printed re-run carries none of the + # restart's fences, and while the intent is `starting` it exits 7. + assert f"with: LANES_LANE={LANE} " in result.stderr, result.stderr + assert "only once this restart is confirmed ready" in result.stderr, result.stderr assert "Preserved task." in box.claude_runs() assert source.read_bytes() == before, "the refused stamp wrote nothing" assert published(box, rel) == before, "and published nothing" From 67e6da5cfd23f3d905875b538ea45b337eab6d59 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Sun, 8 Nov 2026 14:21:56 +0000 Subject: [PATCH 09/19] lane-handoff's --restart respawn carries the caller's LANES_WORKSPACE_ROOT, LANES_REPO and LANES_FILE beside LANES_ROUTE_WRITES, so a /ctx with a register of its own resolves it again in the new pane (opensoft/openRepoTools#198, slice 2b) Lane openRepoTools-3's review of opensoft/openRepoTools#316, NIT 729. The respawn line carried LANES_ROUTE_WRITES but not the register's own seams, so the supervisor and the lane-start it runs resolved the default W, and a /ctx with a W or a register of its own failed closed. That predates slice 2b. Now each of the three is carried where it is set, quoted as the others are; lane-handoff never sets one of its own. Its environment doc names them. New case, red at the parent: test_ctx_respawn_carries_the_register_seams. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 --- lane-handoff | 11 +++++++++++ tests/test_supervised_restart_regressions.py | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/lane-handoff b/lane-handoff index 02f7594..af2d394 100755 --- a/lane-handoff +++ b/lane-handoff @@ -112,6 +112,9 @@ # handoff's included, go through its own register # checkout `-lanes//`, and a `--restart` # respawn carries it to the supervisor +# $LANES_WORKSPACE_ROOT, $LANES_REPO, $LANES_FILE the register's seams, read +# by the helper; a `--restart` respawn carries +# each one that is set # $LANES_AGENT this agent's name for the record (default: claude) # $PCLAUDE the launcher (default: lclaude, else pclaude, else claude-profile) # $LANE_HANDOFF_NO_TMUX=1 never touch tmux (the identity fix, --exit, --restart) @@ -3145,7 +3148,15 @@ if [ "$do_restart" = 1 ]; then # supervisor's `lane-start` compares the row's file with it. Under the # opt-in that is the lane's own `-lanes//`; a respawn without it # would resolve W's copy instead and refuse the restart as two handoffs. + # AND THE REGISTER'S OWN SEAMS (lane openRepoTools-3's review of #316, + # 729), for the same reason: a caller's `LANES_WORKSPACE_ROOT`, `LANES_REPO` + # or `LANES_FILE` is what this command's reads and writes resolved, and + # this command never sets one of its own. Without them the new pane + # resolves the default W, and a `/ctx` with a W of its own fails closed. [ -z "${LANES_ROUTE_WRITES:-}" ] || launch_line="LANES_ROUTE_WRITES=$(quoted "$LANES_ROUTE_WRITES") $launch_line" + [ -z "${LANES_FILE:-}" ] || launch_line="LANES_FILE=$(quoted "$LANES_FILE") $launch_line" + [ -z "${LANES_REPO:-}" ] || launch_line="LANES_REPO=$(quoted "$LANES_REPO") $launch_line" + [ -z "${LANES_WORKSPACE_ROOT:-}" ] || launch_line="LANES_WORKSPACE_ROOT=$(quoted "$LANES_WORKSPACE_ROOT") $launch_line" say " respawning $target: $launch_line" if tmux respawn-pane -k -t "$target" "$launch_line" 2>/dev/null; then say " respawned into the supervisor — it launches the fresh session, confirms it came up, and stays in this pane if it did not" diff --git a/tests/test_supervised_restart_regressions.py b/tests/test_supervised_restart_regressions.py index c516839..13fc922 100644 --- a/tests/test_supervised_restart_regressions.py +++ b/tests/test_supervised_restart_regressions.py @@ -1278,6 +1278,24 @@ def test_signal_after_launcher_exit_keeps_live_or_unknown_holder_indeterminate(r if proc.poll() is None: proc.wait(timeout=10) +def test_ctx_respawn_carries_the_register_seams(restart_box): + """729 of the review of #316: the respawn line carries the caller's + `LANES_WORKSPACE_ROOT`, `LANES_REPO` and `LANES_FILE` beside + `LANES_ROUTE_WRITES`, so the supervisor and the `lane-start` it runs + resolve the register this `lane-handoff` wrote through, and a `/ctx` + with a W of its own no longer fails closed in the new pane.""" + box = restart_box + prepare_restart(box) + assert helper(box, "set-restart-intent", LANE, "ready", "--expect", "pending").returncode == 0 + box.env["LANES_WORKSPACE_ROOT"] = str(box.wip) + result = supervisor(box, "--restart", "--lane", LANE, "--dir", str(box.lane_dir), "clear") + assert result.returncode == 0, (result.stdout, result.stderr) + respawn = box.tmux_log.read_text() + assert "respawn-pane" in respawn, respawn + for key in ("LANES_WORKSPACE_ROOT", "LANES_REPO", "LANES_FILE"): + assert f"{key}={box.env[key]} " in respawn, (key, respawn) + + def test_respawn_restores_configured_roots_in_new_pane(restart_box): box = restart_box protocol = box.root / "protocol with spaces" From 2bd3ea2932dd578591126632d519814de3a11800 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Sun, 8 Nov 2026 14:34:43 +0000 Subject: [PATCH 10/19] lane-start's duplicate-row refusal, under the opt-in, calls a duplicate only W's working tree shows a stray in W, never a hand edit and commit in W, and sends a published one to the lane's own checkout's register and the publish act from there (opensoft/openRepoTools#198, slice 2b) Lane openRepoTools-3's review of opensoft/openRepoTools#316, NIT 730. Under LANES_ROUTE_WRITES=1 with the default resolution, the rows are read from W, and the refusal said to merge the rows "by hand in" W's register and commit with `lanes-edit.sh commit`, which under the opt-in commits in the lane's own checkout, not in W. W is the readers' copy, where no tool writes (Amendment 23 S5). Now, under the opt-in: - where origin/, as W last fetched it, has fewer than two such rows, the duplicate is only in W's working tree, and the refusal names it a stray in W, which a person takes out; - where origin carries the duplicate, the merge is a hand edit of the lane's own checkout's register (`lane-checkout ` names it, or `--create` makes it), published from there with `LANES_LANE= LANES_ROUTE_WRITES=1 lanes-edit.sh commit`. Without the opt-in the words are unchanged. The second arm is not reached with this helper: `canon-lane` reads the published register after W is fast-forwarded and refuses a published duplicate first (Amendment 15(d)), and where it cannot answer, `lane-dir` and the managed-ledger fence refuse it too. So under the opt-in this refusal meets the stray in W, and the second arm is the right words for a path that reaches it anyway; the comment says so. New case, red at the parent: test_duplicate_row_only_in_W_is_named_a_stray_there. And a pin, green at the parent too: test_published_duplicate_row_is_refused_before_W_is_named (canon-lane's refusal, nothing written, never the words that send a merge to W). Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 --- lane-start | 31 +++++++++++++++++++++ tests/test_a23_lane_start.py | 54 ++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/lane-start b/lane-start index cf4be3e..4efb0c2 100755 --- a/lane-start +++ b/lane-start @@ -1702,6 +1702,37 @@ if [ "$row_count" -gt 1 ]; then # is the rule this repository already learned once. rfl_why="spelled ${rfl_names}— a lane name is ONE name under any case (Amendment 15), so those are two rows for ONE lane, and lanes-edit.sh can edit neither" fi + # UNDER THE OPT-IN A ROW IS NEVER MERGED IN W (lane openRepoTools-3's review + # of #316, 730). `$LANES_FILE` is W here, the readers' copy, where no tool + # writes (Amendment 23 S5), and the publish act commits in the lane's own + # checkout, not in W. So where origin/, as W last fetched it, carries + # fewer than two such rows, the duplicate is only in W's working tree: a + # stray there, which a person takes out. Where origin carries it, the merge + # is a hand edit of the lane's own checkout's register, published from there. + # (With this helper a published duplicate is refused before this point: + # `canon-lane` above and the managed-ledger fence read the published register. + # So the stray is the case met here, and the second arm is the right words + # for a path that reaches it anyway.) + if [ "$LS_ROUTED" = 1 ]; then + rfl_br="${LANES_BRANCH:-main}" + rfl_pub="$row_count" + if [ "$LS_ROWS_FROM" != ref ]; then + rfl_pub="" + if rfl_text="$(git -C "$LANES_REPO" show "refs/remotes/origin/$rfl_br:lanes/LANES.md" 2>/dev/null)"; then + rfl_pub="$(LS_ROWS_FROM=ref; LS_ROWS_TEXT="$rfl_text"; rows_for_lane "$LANE" | grep -c . || :)" + fi + fi + if [ -n "$rfl_pub" ] && [ "$rfl_pub" -lt 2 ]; then + die "W's working tree has $row_count rows for lane $LANE, and origin/$rfl_br, as W last fetched it, has $rfl_pub, so the duplicate is a stray in W ($LANES_FILE), the readers' copy, where no tool writes (Amendment 23 S5; \`$LANES_EDIT refresh-workspace\` names it). Taking it out of W is a person's act; then re-run. Nothing was launched, renamed or written." 2 + fi + rfl_co="$("$LANES_EDIT" lane-checkout "$LANE" 2>/dev/null || :)" + if [ -n "$rfl_co" ]; then + rfl_where="the lane's own checkout's register, $rfl_co/lanes/LANES.md," + else + rfl_where="the lane's own checkout's register, lanes/LANES.md in the checkout that LANES_ROUTE_WRITES=1 $LANES_EDIT lane-checkout $LANE --create makes and names," + fi + die "the register has $row_count rows for lane $LANE, $rfl_why. Merge them into one row by hand in $rfl_where never in W ($LANES_FILE), the readers' copy, where no tool writes (Amendment 23 S5) — Amendment 15(d): append the newer row's session id(s) to the older row's session cell, in order, and remove the newer row in the SAME commit, whose message names both spellings — then publish it from there with 'LANES_LANE=$LANE LANES_ROUTE_WRITES=1 lanes-edit.sh commit \"merged duplicate rows\"' and re-run" 2 + fi die "the register has $row_count rows for lane $LANE, $rfl_why. Merge them into one row by hand in $LANES_FILE (one line each) — Amendment 15(d): append the newer row's session id(s) to the older row's session cell, in order, and remove the newer row in the SAME commit, whose message names both spellings — then commit with 'LANES_LANE=$LANE lanes-edit.sh commit \"merged duplicate rows\"' and re-run" 2 fi diff --git a/tests/test_a23_lane_start.py b/tests/test_a23_lane_start.py index 21c6b0c..76f4e01 100644 --- a/tests/test_a23_lane_start.py +++ b/tests/test_a23_lane_start.py @@ -399,3 +399,57 @@ def test_refused_stamp_names_a_checkout_copy_behind_origin(tmp_path, short_root) assert f"git -C {checkout} merge --ff-only origin/main" in said[0], said assert "own words" in start.stdout, "the first prompt is the checkout's copy, as said" assert "NEWER BLOCK" not in start.stdout + + +def _duplicate_row(register: str) -> str: + """The register with the lane's row written twice, one line each.""" + lines = register.splitlines(keepends=True) + found = [i for i, r in enumerate(lines) if r.startswith(f"| `{LANE}`")] + assert len(found) == 1, found + return "".join(lines[:found[0] + 1] + [lines[found[0]]] + lines[found[0] + 1:]) + + +def _duplicate_refusal(stderr: str) -> str: + said = [line for line in stderr.splitlines() if f"rows for lane {LANE}" in line] + assert len(said) == 1, stderr + return said[0] + + +def test_duplicate_row_only_in_W_is_named_a_stray_there(tmp_path, short_root): + """730 of lane openRepoTools-3's review of #316: under the opt-in, a + duplicate row that only W's working tree shows is a stray in W, the + readers' copy, where no tool writes (Amendment 23 S5). The refusal says + so, and never sends a merge, or a commit, to W.""" + box, _checkout, _lane_dir, _before = _started(tmp_path, short_root) + register = box.wip / "lanes" / "LANES.md" + register.write_text(_duplicate_row(register.read_text(encoding="utf-8")), encoding="utf-8") + tip = _tip(box) + + start = box.start("--fresh", window_name=LANE) + assert start.returncode == 2, start.stderr + said = _duplicate_refusal(start.stderr) + assert "is a stray in W" in said, said + assert "origin/main, as W last fetched it, has 1" in said, said + assert "Merge them into one row" not in said, said + assert "lanes-edit.sh commit" not in said, said + assert _tip(box) == tip, "nothing was written" + + +def test_published_duplicate_row_is_refused_before_W_is_named(tmp_path, short_root): + """730: under the opt-in, a duplicate row that origin carries never reaches + the words that send a merge to W. `canon-lane` reads the published + register after W is fast-forwarded and refuses the two rows first + (Amendment 15(d)), naming the merge and the push; nothing is written. (A + pin, green at the parent too: it is why the duplicate-row refusal under + the opt-in only ever meets a stray in W.)""" + box, _checkout, _lane_dir, _before = _started(tmp_path, short_root) + _peer_push(box, {"lanes/LANES.md": _duplicate_row(_published(box, "lanes/LANES.md"))}, + "a duplicate row for the lane, from another workstation") + tip = _tip(box) + + start = box.start("--fresh", window_name=LANE) + assert start.returncode == 2, start.stderr + assert f"the PUBLISHED register spells lane {LANE} two ways" in start.stderr, start.stderr + assert "Merge them into one row by hand in" not in start.stderr, start.stderr + assert not [line for line in start.stderr.splitlines() if f"rows for lane {LANE}" in line], start.stderr + assert _tip(box) == tip, "nothing was written" From fb4a7b0775657e2e3dc6f928b615f2c439c4a63b Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Sun, 8 Nov 2026 14:37:50 +0000 Subject: [PATCH 11/19] The t018 modules' docstrings name slice 2b's one exception to "byte for byte what c45a452 printed": lane-handoff's commit act, re-captured from slice 2b's tools (opensoft/openRepoTools#198, slice 2b) Lane openRepoTools-3's review of opensoft/openRepoTools#316, question 3. Slice 2b replaced lane-handoff's own commit and push with one `write-handoff --prepend`, and moved the two single-repository goldens at exactly those lines (three hunks in all). "Byte for byte what c45a452 printed" was then false for them. Both module docstrings and both `capture()` docstrings now name the exception: which lines, why, and that every other line is c45a452's. The case names are kept. No behaviour changes, so there is no red case and no mutant. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 --- tests/test_triad_lane_tooling.py | 16 +++++++++++++++- tests/test_triad_lane_tooling_handoff.py | 20 ++++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/tests/test_triad_lane_tooling.py b/tests/test_triad_lane_tooling.py index 296412b..a5b5feb 100644 --- a/tests/test_triad_lane_tooling.py +++ b/tests/test_triad_lane_tooling.py @@ -25,6 +25,17 @@ normalised for what no two runs share - the bench's path, UTC stamps, hashes, ages and byte sizes - and for nothing else. +ONE EXCEPTION, BY DESIGN: `lane-handoff`'s commit act (#198, act 2's slice +2b). That slice replaces `lane-handoff`'s own commit and push in the +register's working tree with one `lanes-edit.sh write-handoff --prepend` +under the checkout's lock. So in the one hunk where repoS-1's handoff is +committed, `handoff commit: ` and `handoff pushed to origin/main` +are the helper's own words now (`lanes-edit: committed: `, +`lanes-edit: pushed origin/main (attempt 1)`), the subject carries the +handoff's why, and a `handoff written through write-handoff` step follows +`handoff refreshed:`. That hunk was re-captured from slice 2b's tools by +`--capture`; every other line is `c45a452`'s, and the case keeps its name. + NOTHING REAL IS TOUCHED: every repository, origin, register, `$HOME` and state directory is under `tmp_path`, and the network is never reached. """ @@ -1220,7 +1231,10 @@ def test_t018_add_refuses_where_the_manifest_cannot_be_read(triad): def capture(tools: Path, golden: Path) -> None: """Write the golden file from the tools in `tools` - run ONCE, against - `c45a452`'s (`git archive c45a452 | tar -x -C `), and committed.""" + `c45a452`'s (`git archive c45a452 | tar -x -C `), and committed. + Slice 2b of #198 ran it again against its own tools for its one exception, + `lane-handoff`'s commit act (the module docstring), and kept every other + line of `c45a452`'s.""" with tempfile.TemporaryDirectory(prefix="t018-capture.") as tmp: text = render_golden(single_repository(Bench(Path(tmp) / "bench", tools))) golden.parent.mkdir(parents=True, exist_ok=True) diff --git a/tests/test_triad_lane_tooling_handoff.py b/tests/test_triad_lane_tooling_handoff.py index 2857829..ee81428 100644 --- a/tests/test_triad_lane_tooling_handoff.py +++ b/tests/test_triad_lane_tooling_handoff.py @@ -29,7 +29,20 @@ checkout). That "before" is STORED (`tests/fixtures/t018-handoff/`), generated ONCE from `c45a452`'s own tools by this module's `--capture`, because CI checks out at depth 1; `tests/test_triad_lane_tooling.py`'s own golden covers the -ordinary single repository and keeps passing unregenerated. +ordinary single repository. + +ONE EXCEPTION, BY DESIGN: `lane-handoff`'s commit act (#198, act 2's slice 2b). +That slice replaces `lane-handoff`'s own commit and push in the register's +working tree with one `lanes-edit.sh write-handoff --prepend` under the +checkout's lock. So in the two hunks where a handoff is committed, +`handoff commit: ` and `handoff pushed to origin/main` are the +helper's own words now (`lanes-edit: committed: `, +`lanes-edit: pushed origin/main (attempt 1)`), the subject carries the +handoff's why, and a `handoff written through write-handoff` step follows +`handoff refreshed:`. Those hunks were re-captured from slice 2b's tools by +`--capture`, as was the one hunk of the same act in +`tests/test_triad_lane_tooling.py`'s golden; every other line is `c45a452`'s, +and the cases keep their names. NOTHING REAL IS TOUCHED: every repository, origin, register, `$HOME` and state directory is under `tmp_path`, and the network is never reached. @@ -1278,7 +1291,10 @@ def test_t018_single_repository_handoff_and_reconcile_are_c45a452s_byte_for_byte def capture(tools: Path, golden: Path) -> None: """Write the golden file from the tools in `tools` - run ONCE, against - `c45a452`'s (`git archive c45a452 | tar -x -C `), and committed.""" + `c45a452`'s (`git archive c45a452 | tar -x -C `), and committed. + Slice 2b of #198 ran it again against its own tools for its one exception, + `lane-handoff`'s commit act (the module docstring), and kept every other + line of `c45a452`'s.""" with tempfile.TemporaryDirectory(prefix="t018-handoff-capture.") as tmp: text = render_golden(single_repository_decoys(Bench(Path(tmp) / "bench", tools))) golden.parent.mkdir(parents=True, exist_ok=True) From c1f2928a357e67aa15cca0f9dcb4e196f1053be1 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Sun, 8 Nov 2026 14:48:23 +0000 Subject: [PATCH 12/19] lane-handoff's register PAUSED line says what the handoff write did, in the skill's phrases, and both scripts' exit-6 words point at the helper's account instead of saying a commit is kept on HEAD of the lane's checkout (opensoft/openRepoTools#198, slice 2b) opensoft/openRepoTools#303's fit (c) in lane openRepoTools-3's review of opensoft/openRepoTools#316, and from its review of opensoft/openRepoTools#311 at 1913db9: question 6, SHOULD 982 (which names lane-handoff:2689) and INFO 990. The register's file-level PAUSED line ended "; handoff refreshed" whatever happened. Now it keys on what `write-handoff` did, with the skill's phrases, so the two writers of the one act (Amendment 17(a)) say the same thing: - "handoff refreshed": it wrote the block and exited 0, git allowed; - "handoff written, NOT published (write-handoff exited n)": 2, 6, and any other code that wrote; "(LANES_NO_GIT=1)" for 990's harness case; - "handoff written, its publication NOT confirmed (write-handoff exited 3)": 3 only; - "handoff NOT refreshed (...)": no handoff found, handoff-path failed, write-handoff not called, or it exited n and wrote nothing. No reader parses the suffix. Exit 6 is not one state: `git add` or `commit` failed and nothing is committed, or the commit is kept on HEAD and named; and with slice 2a's 604, an 8(d) handoff's commit or push in its own repository failed. Both scripts' 6 words now say the file is written and NOT published, point at the helper's words for whether and where a commit was made, and name the publish act only for a commit kept in the lane's register checkout. The shell suite's RV-B2 (a) assertion moves with lane-start's words. test_handoff_skill_80_85's reading of lane-handoff's register line sets the record of a write that exited 0. New cases, red at the parent: test_register_paused_line_says_what_the_write_did[2, 3, 6, 1] and test_stamp_exit_6_says_what_the_helper_said_not_kept_on_HEAD (which also pins the battery's survivor LS45); test_no_git_handoff_says_written_and_ nothing_committed asserts the LANES_NO_GIT=1 record. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 --- lane-handoff | 27 +++++++++++++++-- lane-start | 7 ++++- tests/test_a23_lane_handoff.py | 49 +++++++++++++++++++++++++++++++ tests/test_a23_lane_start.py | 32 ++++++++++++++++++++ tests/test_handoff_skill_80_85.py | 2 +- tests/test_lane_helpers.sh | 3 +- 6 files changed, 114 insertions(+), 6 deletions(-) diff --git a/lane-handoff b/lane-handoff index af2d394..e98ffd5 100755 --- a/lane-handoff +++ b/lane-handoff @@ -2649,7 +2649,18 @@ subject_text() { # # `--restart` pass its gate and respawn the pane over a STALE top block, which # is the one document the new session is started with. handoff_written=0 +# WHAT THIS ACT DID, AS THE REGISTER'S FILE-LEVEL PAUSED LINE SAYS IT (4(b) +# below; #303's fit (c) in lane openRepoTools-3's review of #316, and its +# question 6 on #311). `handoff refreshed` only where `write-handoff` wrote the +# block and exited 0 with git allowed. Otherwise the skill's phrases, so the +# two writers of the one act (Amendment 17(a)) say the same thing: written +# and NOT published (2, 6, any other code that wrote, and LANES_NO_GIT=1), +# written and its publication NOT confirmed (3 only), or NOT refreshed and +# why. No reader parses it; it never claims what was not done. +handoff_record="handoff NOT refreshed (write-handoff was not called)" if [ -z "$handoff_file" ]; then + handoff_record="handoff NOT refreshed (no handoff found)" + [ "$hp_rc" = 0 ] || [ "$hp_rc" = 8 ] || handoff_record="handoff NOT refreshed (handoff-path exited $hp_rc)" note "the row names no handoff this command could find${handoff_cell:+ ('$handoff_cell')}, so the top block was NOT written. The record below is still written; put this block at the head of the lane's handoff by hand:" top_block >&2 else @@ -2684,6 +2695,7 @@ else # its read-back name this path, as the helper resolved it. handoff_file="${hs_line%% *}" handoff_written=1 + handoff_record="handoff written, NOT published (write-handoff exited $hs_rc)" step "handoff refreshed: $handoff_file (+$hs_added lines at the head)" case "$hs_rc" in # THE WORDS SAY WHAT WAS DONE (lane openRepoTools-3's review of @@ -2691,16 +2703,25 @@ else # LANES_NO_GIT=1 the helper writes the file, commits nothing and # exits 0. 0) if [ "${LANES_NO_GIT:-0}" = 1 ]; then + handoff_record="handoff written, NOT published (LANES_NO_GIT=1)" step "handoff written through \`write-handoff\`, which exited 0 under LANES_NO_GIT=1: the block is written in $handoff_file and nothing is committed or pushed" else + handoff_record="handoff refreshed" step "handoff written through \`write-handoff\`, which exited 0: its words above say what it committed and pushed" fi ;; 2) note "the handoff is written in $handoff_file and NOT committed (\`write-handoff\` exited 2: the checkout refused, and its words above say why and give the commit to make once that is answered). Never re-run this handoff to commit it: a second run puts a second block on top." ;; - 3) note "the handoff is written in $handoff_file and its commit pushed, and origin did not confirm it (\`write-handoff\` exited 3; its words above name the commit): look before writing it again, because a second block on top is a second handoff in the one document this protocol never rewrites." ;; - 6) note "the handoff is written in $handoff_file and committed, and the commit is KEPT on HEAD of that checkout, not pushed (\`write-handoff\` exited 6; its words above name it). No later write publishes it, and each one refuses until it is published (Amendment 23 S3): once those words are answered, LANES_LANE=$lane $LANES_EDIT commit \"\", the publish act." ;; + 3) handoff_record="handoff written, its publication NOT confirmed (write-handoff exited 3)" + note "the handoff is written in $handoff_file and its commit pushed, and origin did not confirm it (\`write-handoff\` exited 3; its words above name the commit): look before writing it again, because a second block on top is a second handoff in the one document this protocol never rewrites." ;; + # 6 IS NOT ONE STATE (lane openRepoTools-3's review of #311, 982, + # which names this line): `git add` or `commit` failed and nothing is + # committed, or the commit is kept on HEAD and named; and with slice + # 2a's 604, an 8(d) handoff's commit or push in its own repository + # failed. The helper's words say which, so these do not guess. + 6) note "the handoff is written in $handoff_file and NOT published (\`write-handoff\` exited 6; its words above say whether a commit was made, where it is kept, and the command that finishes it). A commit kept unpushed in this lane's register checkout is published by no later write, and each write there refuses until it is (Amendment 23 S3): once those words are answered, LANES_LANE=$lane $LANES_EDIT commit \"\", the publish act." ;; *) note "the handoff is written in $handoff_file and was NOT published (\`write-handoff\` exited $hs_rc; its words above say what is left)." ;; esac else + handoff_record="handoff NOT refreshed (write-handoff exited $hs_rc and wrote nothing)" note "the refreshed top block could NOT be written to $handoff_file (\`write-handoff\` exited $hs_rc; its words are above), so that file still carries the block it had. It is printed below; put it at the head of that file by hand:" top_block >&2 fi @@ -2905,7 +2926,7 @@ else # and no later line corrects any of them (Amendment 11 clause (e)); a # register line naming it would be the fifth, one file along. if [ -n "$uuid" ]; then - LANES_LANE="$lane" "$LANES_EDIT" append-line "PAUSED — lane $lane, session $uuid@$ws, $UTC, $why_text; handoff refreshed" >/dev/null 2>&1 || + LANES_LANE="$lane" "$LANES_EDIT" append-line "PAUSED — lane $lane, session $uuid@$ws, $UTC, $why_text; $handoff_record" >/dev/null 2>&1 || note "the LANES.md file-level PAUSED line was not appended (the register may be busy or behind); the object-log line above is the record readers take." else note "no transcript uuid is knowable for this lane, so neither the object log's PAUSED nor the register's file-level line was written — a record naming 'unknown' in that field is a record no restart can read (Amendment 11 clause (e)). The handoff file above is refreshed and is the whole of what this act left behind. Bind the session first: lane-start --no-launch " diff --git a/lane-start b/lane-start index 4efb0c2..a807d1d 100755 --- a/lane-start +++ b/lane-start @@ -4446,7 +4446,12 @@ if [ "$lane_verb" = RESUMED ] && [ "$row_count" = 1 ] && (( stamp_ok )); then step "handoff stamped, committed and pushed: $hs_written: $hs_stamp" fi ;; 3) note "Rule 3's stamp is written in $hs_written and its commit pushed, and origin did not confirm it (\`write-handoff\` exited 3; its words above name the commit): look before writing it again, because a stamp written twice is two resumes in the one document this protocol never rewrites." ;; - 6) note "Rule 3's stamp is written in $hs_written and committed, and the commit is KEPT on HEAD of that checkout, not pushed (\`write-handoff\` exited 6; its words above name it). No later write publishes it, and each one refuses until it is published (Amendment 23 S3): once those words are answered, LANES_LANE=$LANE $LANES_EDIT commit \"\", the publish act." ;; + # 6 IS NOT ONE STATE (lane openRepoTools-3's review of #311, 982): + # `git add` or `commit` failed and nothing is committed, or the commit + # is kept on HEAD and named; and with slice 2a's 604, an 8(d) + # handoff's commit or push in its own repository failed. The helper's + # words say which, so these do not guess. + 6) note "Rule 3's stamp is written in $hs_written and NOT published (\`write-handoff\` exited 6; its words above say whether a commit was made, where it is kept, and the command that finishes it). A commit kept unpushed in this lane's register checkout is published by no later write, and each write there refuses until it is (Amendment 23 S3): once those words are answered, LANES_LANE=$LANE $LANES_EDIT commit \"\", the publish act." ;; *) note "Rule 3's stamp is written in $hs_written and was NOT published (\`write-handoff\` exited $hs_rc; its words above say what is left). The launch is not refused for it." ;; esac elif [ "$ri_active" = 1 ] && [ "$hs_rc" = 7 ]; then diff --git a/tests/test_a23_lane_handoff.py b/tests/test_a23_lane_handoff.py index 22ab34a..aeba4d4 100644 --- a/tests/test_a23_lane_handoff.py +++ b/tests/test_a23_lane_handoff.py @@ -32,6 +32,8 @@ import subprocess from pathlib import Path +import pytest + from conftest import REPO from test_a23_lane_start import (HANDOFF, _assert_nothing_unpublished, _assert_W_only_fast_forwarded, _cell, _edit, _origin, _peer_push, _published, _started, _tip) @@ -175,3 +177,50 @@ def test_no_git_handoff_says_written_and_nothing_committed(tmp_path, short_root) said = [line for line in result.stderr.splitlines() if "LANES_NO_GIT=1" in line and "nothing is committed" in line] assert len(said) == 1, result.stderr + # 990 of the review of #311, on 2b: the register's PAUSED line says so too. + register = (checkout / "lanes" / "LANES.md").read_text(encoding="utf-8") + paused = [line for line in register.splitlines() if f"PAUSED — lane {LANE}, session " in line] + assert len(paused) == 1, register + assert paused[0].endswith("; handoff written, NOT published (LANES_NO_GIT=1)"), paused + + +def _forcing(box: Sandbox, rc: int, write: bool) -> Path: + """A `LANES_EDIT` whose `write-handoff` exits `rc`: after the real verb has + written, committed and pushed (`write`), or at once, having written + nothing. Every other verb is the real helper's.""" + real = box.bin / "lanes-edit.sh" + wrapper = box.root / f"forcing-lanes-edit-{rc}.sh" + first = f"'{real}' \"$@\"; " if write else "" + wrapper.write_text(f"""#!/usr/bin/env bash +if [ "${{1-}}" = write-handoff ]; then {first}exit {rc}; fi +exec '{real}' "$@" +""", encoding="utf-8") + wrapper.chmod(0o755) + return wrapper + + +@pytest.mark.parametrize(("rc", "write", "record"), [ + (2, True, "handoff written, NOT published (write-handoff exited 2)"), + (3, True, "handoff written, its publication NOT confirmed (write-handoff exited 3)"), + (6, True, "handoff written, NOT published (write-handoff exited 6)"), + (1, False, "handoff NOT refreshed (write-handoff exited 1 and wrote nothing)"), +]) +def test_register_paused_line_says_what_the_write_did(tmp_path, short_root, rc, write, record): + """#303's fit (c) in lane openRepoTools-3's review of #316, and its + question 6 on #311: the register's file-level PAUSED line says + `handoff refreshed` only where `write-handoff` wrote the block and exited + 0. Otherwise it uses the skill's phrases: written and NOT published (2, + 6), its publication NOT confirmed (3 only), or NOT refreshed and why. And + 982: exit 6's words no longer say the commit is kept on HEAD of the lane's + checkout, since a 6 may have made no commit, or (2a's 604) made it in an + 8(d) handoff's own repository.""" + box, _checkout, _cell_, _before = _ready(tmp_path, short_root) + + result = _handoff(box, env={"LANES_EDIT": str(_forcing(box, rc, write))}) + register = _published(box, "lanes/LANES.md") + paused = [line for line in register.splitlines() if f"PAUSED — lane {LANE}, session " in line] + assert len(paused) == 1, (register, result.stderr) + assert paused[0].endswith(f"; {record}"), (paused, result.stderr) + if rc == 6: + assert "KEPT on HEAD" not in result.stderr, result.stderr + assert "its words above say whether a commit was made, where it is kept" in result.stderr, result.stderr diff --git a/tests/test_a23_lane_start.py b/tests/test_a23_lane_start.py index 76f4e01..3b88902 100644 --- a/tests/test_a23_lane_start.py +++ b/tests/test_a23_lane_start.py @@ -453,3 +453,35 @@ def test_published_duplicate_row_is_refused_before_W_is_named(tmp_path, short_ro assert "Merge them into one row by hand in" not in start.stderr, start.stderr assert not [line for line in start.stderr.splitlines() if f"rows for lane {LANE}" in line], start.stderr assert _tip(box) == tip, "nothing was written" + + +def test_stamp_exit_6_says_what_the_helper_said_not_kept_on_HEAD(tmp_path, short_root): + """982 of lane openRepoTools-3's review of #311, which 2b's words share: + `write-handoff`'s 6 is not one state. `git add` or `commit` failed and + nothing is committed, or the commit is kept on HEAD and named; and with + 2a's 604, an 8(d) handoff's commit or push in its own repository failed. + So lane-start's note points at the helper's words, and names the publish + act only for a commit kept in this lane's register checkout. (It also + pins the battery's survivor LS45: the 6 arm falling to the generic note.)""" + box, _checkout, _lane_dir, _before = _started(tmp_path, short_root) + cell = _cell(box) + _peer_push(box, {cell: HANDOFF}, "the lane's handoff, written elsewhere") + real = box.bin / "lanes-edit.sh" + wrapper = box.root / "six-lanes-edit.sh" + wrapper.write_text(f"""#!/usr/bin/env bash +if [ "${{1-}}" = write-handoff ]; then '{real}' "$@"; exit 6; fi +exec '{real}' "$@" +""", encoding="utf-8") + wrapper.chmod(0o755) + + start = subprocess.run([str(box.bin / "lane-start"), "repoZ", "1", "--no-launch", "--fresh"], + env=dict(box.env, LANES_EDIT=str(wrapper), FAKE_TMUX_WINDOW_NAME=LANE), + capture_output=True, text=True, timeout=180, check=False) + assert start.returncode == 0, start.stderr + said = [line for line in start.stderr.splitlines() if "`write-handoff` exited 6" in line] + assert len(said) == 1, start.stderr + assert "KEPT on HEAD" not in said[0], said + assert "and NOT published" in said[0], said + assert "its words above say whether a commit was made, where it is kept" in said[0], said + assert f"LANES_LANE={LANE} " in said[0], said + assert "the publish act" in said[0], said diff --git a/tests/test_handoff_skill_80_85.py b/tests/test_handoff_skill_80_85.py index abf66de..f810943 100644 --- a/tests/test_handoff_skill_80_85.py +++ b/tests/test_handoff_skill_80_85.py @@ -224,7 +224,7 @@ def _lane_handoff_writes(box: Sandbox, why: str, workstation and instant the skill is run with.""" script = ('why="$STUB_WHY"; handoff_kind="$STUB_KIND"\n' + lane_handoff_why_text() + - f'lane={LANE}; uuid={UUID}; ws={WS}; UTC={NOW}\n' + f'lane={LANE}; uuid={UUID}; ws={WS}; UTC={NOW}; handoff_record="handoff refreshed"\n' f'printf \'%s\\0%s\\0\' "$why_text" {lane_handoff_register_line()}\n') proc = box.run("bash", script, _why_env(why, kind or "unknown")) assert proc.returncode == 0, (proc.returncode, proc.stderr) diff --git a/tests/test_lane_helpers.sh b/tests/test_lane_helpers.sh index 07d20cb..2bb4469 100755 --- a/tests/test_lane_helpers.sh +++ b/tests/test_lane_helpers.sh @@ -3752,7 +3752,8 @@ is "a handoff stamp whose PUSH is rejected still exits 0 — the record is not has "…the stamp really written and committed all the same" "$(cat "$WIP/handoffs/repoCN/a.md")" "RESUMED by $CN_A (lane repoCN-1) at " has "…in its own commit, which is the one nothing pushed" "$(git -C "$WIP" log --format=%s -n1)" "handoff(repoCN-1@" has "…the helper saying why nothing was replayed, and that the commit is kept" "$err" "refused with origin/main unmoved, so this is no lost race and a replay would meet the same refusal; the commit is local and kept on HEAD" -has "…and lane-start naming the publish act, once those words are answered" "$err" "the commit is KEPT on HEAD of that checkout, not pushed" +has "…and lane-start pointing at those words, which say where the commit is kept" "$err" "its words above say whether a commit was made, where it is kept, and the command that finishes it" +has "…and naming the publish act, once those words are answered" "$err" "commit \"\", the publish act" hasnt "…never promising that a later write carries it: S3 refuses every one" "$err" "carries this commit with it" # The fixture publishes it, as its owner would, and the sections below start # from a checkout with nothing unpublished (S3). From 2e4d7dd673837d07a7d9bc18c0199067a5487fb5 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Sun, 8 Nov 2026 15:17:37 +0000 Subject: [PATCH 13/19] lane-start's first prompt reads the file its stamp wrote, so a handoff a peer moved with its row's cell during the stamp is still delivered; the slice's tests split their composite asserts, share one copy of each W helper, and scrub a caller's WORKBENCHES_ window (opensoft/openRepoTools#198, slice 2b) rv2b's NITs on opensoft/openRepoTools#316. NIT 1. Where a peer moves the lane's handoff, and the row's cell with it, while this launch stamps it, the write's replay follows the cell and stamps the moved file. 5d then read the path `handoff-path` named before the write, which is gone from the checkout, and launched with no first prompt. Now 5d reads the file this call wrote (`HS_PATH` from the helper's written-file line, where that file is there). New case, red at the parent: test_first_prompt_follows_a_handoff_a_peer_moved_during_the_ stamp, whose wrapper's peer `git mv`s the handoff and edits the row's cell during `write-handoff`. NIT 2. Fifteen composite asserts (S9073) are split into one condition each: six in test_a23_lane_start, four in test_a23_lane_handoff and five in the supervised-restart cases. NIT 3. One copy of each helper: test_a23_lane_start's `_reflog` and `_assert_W_only_fast_forwarded` give way to slice 1a's `W_reflog` and `assert_W_moved_only_by_fast_forward` in test_a23_lane_start_read, which check the same three things; a new `no_auto_gc` there replaces `_quiet` and both gc loops; `FF` is dropped. `_published` and `published` stay two: the row-stamp sandbox has no `origin` and returns text, the claude-current one returns bytes. NIT 5. `WORKBENCHES_` joins both SCRUBBED_PREFIXES, so a caller's own lane window never reaches a sandbox. NIT 4 needs no change here: slice 3 already spells the PAUSED subject as lane-handoff does (`${ws:-unknown}`, control characters made spaces). Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 --- lane-start | 4 + tests/test_a23_lane_handoff.py | 20 ++-- tests/test_a23_lane_start.py | 99 +++++++++++++------- tests/test_a23_lane_start_read.py | 8 ++ tests/test_lane_start_claude_current.py | 2 +- tests/test_lane_start_row_stamp.py | 2 +- tests/test_supervised_restart_regressions.py | 22 +++-- 7 files changed, 102 insertions(+), 55 deletions(-) diff --git a/lane-start b/lane-start index a807d1d..d90c82a 100755 --- a/lane-start +++ b/lane-start @@ -4433,6 +4433,10 @@ if [ "$lane_verb" = RESUMED ] && [ "$row_count" = 1 ] && (( stamp_ok )); then hs_line="$(printf '%s\n' "$hs_out" | awk -F'\t' 'NF >= 2 && $1 != "" { l = $0 } END { print l }')" if [ -n "$hs_line" ]; then hs_written="${hs_line%% *}" + # 5d READS THE FILE THIS CALL WROTE (rv2b's NIT 1 on #316): a peer that + # moved the handoff and its cell on origin is followed by the write's + # replay, and the path `handoff-path` named before it may be gone. + if [ -f "$hs_written" ]; then HS_PATH="$hs_written"; fi # THE DIGEST THE RESTART NOW EXPECTS is the stamped file's, which the # helper reserved in the intent before the bytes: 5d reads that file. if [ "$ri_active" = 1 ]; then ri_expected_digest="${hs_line##* }"; fi diff --git a/tests/test_a23_lane_handoff.py b/tests/test_a23_lane_handoff.py index aeba4d4..14afffb 100644 --- a/tests/test_a23_lane_handoff.py +++ b/tests/test_a23_lane_handoff.py @@ -35,8 +35,9 @@ import pytest from conftest import REPO -from test_a23_lane_start import (HANDOFF, _assert_nothing_unpublished, _assert_W_only_fast_forwarded, +from test_a23_lane_start import (HANDOFF, _assert_nothing_unpublished, _cell, _edit, _origin, _peer_push, _published, _started, _tip) +from test_a23_lane_start_read import assert_W_moved_only_by_fast_forward from test_lane_start_row_stamp import LANE, Sandbox, pytestmark, short_root # noqa: F401 WHY = "a test handoff" @@ -78,9 +79,11 @@ def test_handoff_written_and_pushed_from_checkout(tmp_path, short_root): published = _published(box, cell) block, sep, rest = published.partition("\n---\n\n") - assert sep and rest == HANDOFF, published + assert sep, published + assert rest == HANDOFF, published assert block.startswith(f"Lane: {LANE} (test-profile, session "), block - assert "## RESUME PROMPT — PAUSED " in block and f"({WHY})" in block, block + assert "## RESUME PROMPT — PAUSED " in block, block + assert f"({WHY})" in block, block assert _origin(box, "log", "-1", "--format=%s", "main", "--", cell).strip() == SUBJECT assert f"Lane: {LANE}" in _origin(box, "log", "-1", "--format=%b", "main", "--", cell) assert f"handoff refreshed: {checkout}/{cell} " in result.stderr, result.stderr @@ -88,7 +91,7 @@ def test_handoff_written_and_pushed_from_checkout(tmp_path, short_root): # THE ONE WRITE IN W IS GONE: W moved only by fast-forward, and its copy of # the handoff took no byte from this act. - _assert_W_only_fast_forwarded(box, before) + assert_W_moved_only_by_fast_forward(box, before) assert (box.wip / cell).read_text(encoding="utf-8") in (HANDOFF, published) assert (checkout / cell).read_text(encoding="utf-8") == published _assert_nothing_unpublished(box, checkout) @@ -125,9 +128,10 @@ def test_S3_refusal_writes_file_no_commit_prints_git_words(tmp_path, short_root) assert "handoffs/repoQ/foreign.md" in result.stderr assert "the handoff is WRITTEN and NOT committed" in result.stderr assert f"commit -m \"{SUBJECT}\"" in result.stderr, result.stderr - assert "NOT committed" in result.stderr and "write-handoff` exited 2" in result.stderr, result.stderr + assert "NOT committed" in result.stderr, result.stderr + assert "write-handoff` exited 2" in result.stderr, result.stderr - _assert_W_only_fast_forwarded(box, before) + assert_W_moved_only_by_fast_forward(box, before) assert (box.wip / cell).read_text(encoding="utf-8") == HANDOFF, "W's copy took no byte" @@ -149,8 +153,8 @@ def test_append_line_carries_LANES_LANE(tmp_path, short_root): made = [line for line in recorded if line.startswith(verb + " ")] assert made == [f"{verb} LANES_LANE={LANE}"], (verb, recorded) register = _published(box, "lanes/LANES.md") - assert f"PAUSED — lane {LANE}, session " in register and f", {WHY} kind unknown; handoff refreshed" in register, \ - register + assert f"PAUSED — lane {LANE}, session " in register, register + assert f", {WHY} kind unknown; handoff refreshed" in register, register assert _published(box, cell).endswith("\n---\n\n" + HANDOFF) _assert_nothing_unpublished(box, checkout) diff --git a/tests/test_a23_lane_start.py b/tests/test_a23_lane_start.py index 3b88902..d0b9113 100644 --- a/tests/test_a23_lane_start.py +++ b/tests/test_a23_lane_start.py @@ -31,21 +31,16 @@ import subprocess from pathlib import Path -from test_a23_lane_start_read import _no_clone_git, default_resolution, rows +from test_a23_lane_start_read import (W_reflog, _no_clone_git, assert_W_moved_only_by_fast_forward, + default_resolution, no_auto_gc, rows) from test_lane_start_row_stamp import LANE, Sandbox, _checkout_of_length, pytestmark, short_root # noqa: F401 HANDOFF = "Lane: repoZ-1 — single-use resume prompt\n\n# state\n\nthe lane's own words\n" -FF = "merge origin/main: Fast-forward" PEER_SID = "bbbb0001-2222-4000-8000-bbbb00012222" # ------------------------------------------------------------------ helpers -def _quiet(box: Sandbox, repo: Path) -> None: - for key, value in (("gc.auto", "0"), ("maintenance.auto", "false")): - box.git("-C", str(repo), "config", key, value) - - def _origin(box: Sandbox, *args: str) -> str: return box.git("--git-dir", str(box.root / "origin.git"), *args) @@ -70,7 +65,7 @@ def _peer(box: Sandbox) -> Path: p = box.root / "peer" if not p.exists(): box.git("clone", "-q", str(box.root / "origin.git"), str(p)) - _quiet(box, p) + no_auto_gc(box, "-C", str(p)) box.git("-C", str(p), "fetch", "-q", "origin", "main") box.git("-C", str(p), "merge", "-q", "--ff-only", "origin/main") return p @@ -91,17 +86,12 @@ def _edit(box: Sandbox, *args: str, env: dict | None = None) -> subprocess.Compl capture_output=True, text=True, timeout=180, check=False) -def _reflog(box: Sandbox) -> list[str]: - return box.git("-C", str(box.wip), "reflog", "--format=%gs", "HEAD").splitlines() - - def _boxed(tmp_path: Path, short_root: Path) -> tuple[Sandbox, Path, Path]: """The sandbox with the default resolution and the opt-in, W kept from gc.""" box = Sandbox(tmp_path) checkout = default_resolution(box) - _quiet(box, box.wip) - for key, value in (("gc.auto", "0"), ("maintenance.auto", "false")): - _origin(box, "config", key, value) + no_auto_gc(box, "-C", str(box.wip)) + no_auto_gc(box, "--git-dir", str(box.root / "origin.git")) lane_dir = _checkout_of_length(short_root, None) box.checkout(lane_dir) return box, checkout, lane_dir @@ -110,21 +100,13 @@ def _boxed(tmp_path: Path, short_root: Path) -> tuple[Sandbox, Path, Path]: def _started(tmp_path: Path, short_root: Path) -> tuple[Sandbox, Path, Path, list[str]]: """A new lane's first start, with W's reflog as it stood before it.""" box, checkout, lane_dir = _boxed(tmp_path, short_root) - before = _reflog(box) + before = W_reflog(box) first = box.start("--dir", str(lane_dir), window_name="claude") assert first.returncode == 0, first.stderr assert (checkout / ".git").is_dir(), "the first start made the lane's checkout" return box, checkout, lane_dir, before -def _assert_W_only_fast_forwarded(box: Sandbox, before: list[str]) -> None: - after = _reflog(box) - assert after[len(after) - len(before):] == before, (before, after) - new = after[:len(after) - len(before)] - assert all(entry == FF for entry in new), new - assert box.git("-C", str(box.wip), "status", "--porcelain") == "", "W is clean" - - def _assert_nothing_unpublished(box: Sandbox, checkout: Path) -> None: cherry = box.git("-C", str(checkout), "cherry", "origin/main", "HEAD") assert not [c for c in cherry.splitlines() if c.startswith("+")], cherry @@ -143,14 +125,16 @@ def test_launch_creates_checkout_and_writes_only_there(tmp_path, short_root): register = _published(box, "lanes/LANES.md") assert sum(1 for r in register.splitlines() if r.startswith(f"| `{LANE}`")) == 1 row = [r for r in register.splitlines() if r.startswith(f"| `{LANE}`")][0] - assert "LIVE · " in row and "RESUMED by " in row, row + assert "LIVE · " in row, row + assert "RESUMED by " in row, row assert f"RESUMED — lane {LANE}, session " in _published(box, f"lanes/log/{LANE}.md") stamped = _published(box, cell) - assert f"(lane {LANE}) at " in stamped and stamped.splitlines()[2].startswith("RESUMED by "), stamped + assert f"(lane {LANE}) at " in stamped, stamped + assert stamped.splitlines()[2].startswith("RESUMED by "), stamped assert _origin(box, "log", "-1", "--format=%s%n%b", "main").startswith(f"handoff({LANE}@Eagle): RESUMED by ") assert f"Lane: {LANE}" in _origin(box, "log", "-1", "--format=%b", "main") - _assert_W_only_fast_forwarded(box, before) + assert_W_moved_only_by_fast_forward(box, before) assert box.git("-C", str(box.wip), "rev-parse", "HEAD").strip() == _tip(box), "W ends on origin's tip" _assert_nothing_unpublished(box, checkout) @@ -159,7 +143,7 @@ def test_launch_fast_forwards_W_before_and_after(tmp_path, short_root): box, checkout, lane_dir = _boxed(tmp_path, short_root) made = _edit(box, "lane-checkout", LANE, "--create") assert made.returncode == 0, made.stderr - before = _reflog(box) + before = W_reflog(box) # Another workstation's start of this lane: its row, and its handoff. register = _published(box, "lanes/LANES.md") row = (f"| `{LANE}` | harness `{PEER_SID}` (minted by lane-start, 2026-10-22T00:00:00Z) | " @@ -174,7 +158,7 @@ def test_launch_fast_forwards_W_before_and_after(tmp_path, short_root): assert len(rows(box.wip / "lanes" / "LANES.md")) == 1, "W was brought up to the peer's row first" assert box.git("-C", str(box.wip), "rev-parse", "HEAD").strip() == _tip(box), "and to the stamp after" assert _origin(box, "log", "-1", "--format=%s", "main").startswith(f"handoff({LANE}@Eagle): RESUMED by ") - _assert_W_only_fast_forwarded(box, before) + assert_W_moved_only_by_fast_forward(box, before) _assert_nothing_unpublished(box, checkout) @@ -223,7 +207,8 @@ def test_the_1312Z_shape_all_three_writes_pushed(tmp_path, short_root): assert start.returncode == 0, start.stderr published = _published(box, "lanes/LANES.md") row = [r for r in published.splitlines() if r.startswith(f"| `{LANE}`")] - assert len(row) == 1 and "RESUMED by " in row[0], row + assert len(row) == 1, row + assert "RESUMED by " in row[0], row assert changed in published, "the peer's change to the row beside it is kept" assert f"RESUMED — lane {LANE}, session " in _published(box, f"lanes/log/{LANE}.md") assert _published(box, cell).splitlines()[2].startswith("RESUMED by "), _published(box, cell) @@ -240,7 +225,8 @@ def test_first_prompt_reads_the_checkout_copy(tmp_path, short_root): start = box.start("--fresh", window_name=LANE) assert start.returncode == 0, start.stderr - assert "RESUMED by " in start.stdout and f"(lane {LANE}) at " in start.stdout, start.stdout + assert "RESUMED by " in start.stdout, start.stdout + assert f"(lane {LANE}) at " in start.stdout, start.stdout assert "STRAY-W-EDIT" not in start.stdout, start.stdout assert "STRAY-W-EDIT" not in _published(box, cell), "W's stray never reaches origin" assert "STRAY-W-EDIT" in stray.read_text(encoding="utf-8"), "and W's stray is a person's to settle" @@ -264,13 +250,14 @@ def test_dry_run_creates_nothing_and_leaves_W(tmp_path, short_root): box.env["LANES_ROUTE_WRITES"] = "1" _peer_push(box, {_cell(box): HANDOFF}, "the lane's handoff, written elsewhere") head = box.git("-C", str(box.wip), "rev-parse", "HEAD") - before = _reflog(box) + before = W_reflog(box) plan = box.start("--fresh", "--dry-run", window_name=LANE) assert plan.returncode == 0, plan.stderr - assert not checkout.exists() and not checkout.parent.exists(), "a dry run makes no lane checkout" + assert not checkout.exists(), "a dry run makes no lane checkout" + assert not checkout.parent.exists(), "and no folder for one" assert box.git("-C", str(box.wip), "rev-parse", "HEAD") == head, "W did not move" - assert _reflog(box) == before + assert W_reflog(box) == before for planned in ("refresh-workspace", f"lane-checkout {LANE} --create", f"write-handoff {LANE} --stamp"): assert any("PLAN" in line and planned in line for line in plan.stderr.splitlines()), (planned, plan.stderr) @@ -295,7 +282,8 @@ def test_publish_failure_no_longer_refuses_a_plain_resume(tmp_path, short_root): capture_output=True, text=True, timeout=180, check=False) assert start.returncode == 0, start.stderr assert "test: write-handoff refused" in start.stderr, start.stderr - assert "RESUMED by " in start.stderr and "write-handoff" in start.stderr + assert "RESUMED by " in start.stderr + assert "write-handoff" in start.stderr assert "Nothing was launched" not in start.stderr assert "RESUMED by " not in _published(box, cell) assert os.path.exists(box.wip / cell) @@ -319,7 +307,7 @@ def test_no_lane_copy_launches_with_no_first_prompt_never_W_copy(tmp_path, short the writes, never through W).""" box = Sandbox(tmp_path) checkout = default_resolution(box, route=False) - _quiet(box, box.wip) + no_auto_gc(box, "-C", str(box.wip)) lane_dir = _checkout_of_length(short_root, None) box.checkout(lane_dir) first = box.start("--dir", str(lane_dir), window_name="claude") @@ -485,3 +473,42 @@ def test_stamp_exit_6_says_what_the_helper_said_not_kept_on_HEAD(tmp_path, short assert "its words above say whether a commit was made, where it is kept" in said[0], said assert f"LANES_LANE={LANE} " in said[0], said assert "the publish act" in said[0], said + + +def test_first_prompt_follows_a_handoff_a_peer_moved_during_the_stamp(tmp_path, short_root): + """rv2b's NIT 1 on #316: a peer moves the lane's handoff, and the row's + cell with it, while this launch stamps it. The write's replay follows the + cell and stamps the moved file, and 5d reads THAT file, not the path + `handoff-path` named before the write, which is gone from the checkout.""" + box, checkout, _lane_dir, _before = _started(tmp_path, short_root) + cell = _cell(box) + _peer_push(box, {cell: HANDOFF}, "the lane's handoff, written elsewhere") + moved = cell[:-len(".md")] + "-moved.md" + peer = _peer(box) + real = box.bin / "lanes-edit.sh" + mark = box.root / "moved.mark" + wrapper = box.root / "moving-lanes-edit.sh" + wrapper.write_text(f"""#!/usr/bin/env bash +if [ "${{1-}}" = write-handoff ] && [ ! -e '{mark}' ]; then + : > '{mark}' + git -C '{peer}' fetch -q origin main && git -C '{peer}' merge -q --ff-only origin/main && + git -C '{peer}' mv -- '{cell}' '{moved}' && + sed -i 's#{cell}#{moved}#' '{peer}/lanes/LANES.md' && + git -C '{peer}' commit -q -am 'a peer moves the handoff' && + git -C '{peer}' push -q origin HEAD:main || exit 99 +fi +exec '{real}' "$@" +""", encoding="utf-8") + wrapper.chmod(0o755) + + start = subprocess.run([str(box.bin / "lane-start"), "repoZ", "1", "--no-launch", "--fresh"], + env=dict(box.env, LANES_EDIT=str(wrapper), FAKE_TMUX_WINDOW_NAME=LANE), + capture_output=True, text=True, timeout=180, check=False) + assert start.returncode == 0, start.stderr + assert mark.exists(), "the peer moved the handoff during the stamp" + assert _cell(box) == moved + assert "RESUMED by " in _published(box, moved) + assert f"{checkout}/{moved}" in start.stderr, start.stderr + assert "launched with no first prompt" not in start.stderr, start.stderr + assert "RESUMED by " in start.stdout, (start.stdout, start.stderr) + assert "own words" in start.stdout, start.stdout diff --git a/tests/test_a23_lane_start_read.py b/tests/test_a23_lane_start_read.py index f50b184..fdb6dba 100644 --- a/tests/test_a23_lane_start_read.py +++ b/tests/test_a23_lane_start_read.py @@ -52,6 +52,14 @@ def rows(register: Path) -> list[str]: if r.startswith(f"| `{LANE}`")] +def no_auto_gc(box: Sandbox, *where: str) -> None: + """No automatic gc or maintenance in the repository `where` names + (`-C ` or `--git-dir `), so a case's reflog and objects stay + what its own acts made.""" + for key, value in (("gc.auto", "0"), ("maintenance.auto", "false")): + box.git(*where, "config", key, value) + + def W_reflog(box: Sandbox) -> list[str]: return box.git("-C", str(box.wip), "reflog", "--format=%gs", "HEAD").splitlines() diff --git a/tests/test_lane_start_claude_current.py b/tests/test_lane_start_claude_current.py index 9bdb262..693c3d1 100644 --- a/tests/test_lane_start_claude_current.py +++ b/tests/test_lane_start_claude_current.py @@ -49,7 +49,7 @@ #: The environment a case must not inherit from the host that runs it: every #: seam `lane-start`, `lanes-edit.sh` and `claude-current` read. SCRUBBED_PREFIXES = ("LANES_", "LANE_START_", "CLAUDE_", "OPENREPOTOOLS_", - "FAKE_", "GIT_", "NPM_CONFIG_", "npm_config_", "TMUX") + "FAKE_", "GIT_", "NPM_CONFIG_", "npm_config_", "TMUX", "WORKBENCHES_") SCRUBBED = {"AGENT_PROTOCOL_ROOT", "PROJECTS_ROOT", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", "PCLAUDE"} diff --git a/tests/test_lane_start_row_stamp.py b/tests/test_lane_start_row_stamp.py index 21670d7..e8ac5d5 100644 --- a/tests/test_lane_start_row_stamp.py +++ b/tests/test_lane_start_row_stamp.py @@ -57,7 +57,7 @@ RUN_SH_HOME = 91 SCRUBBED_PREFIXES = ("LANES_", "LANE_START_", "CLAUDE_", "OPENREPOTOOLS_", - "FAKE_", "GIT_", "TMUX") + "FAKE_", "GIT_", "TMUX", "WORKBENCHES_") SCRUBBED = {"AGENT_PROTOCOL_ROOT", "PROJECTS_ROOT", "XDG_CONFIG_HOME", "XDG_CACHE_HOME"} diff --git a/tests/test_supervised_restart_regressions.py b/tests/test_supervised_restart_regressions.py index 13fc922..81231da 100644 --- a/tests/test_supervised_restart_regressions.py +++ b/tests/test_supervised_restart_regressions.py @@ -14,7 +14,7 @@ import pytest from test_lane_start_claude_current import Sandbox, LANE, _write, pytestmark -from test_a23_lane_start_read import W_reflog, assert_W_moved_only_by_fast_forward, default_resolution +from test_a23_lane_start_read import W_reflog, assert_W_moved_only_by_fast_forward, default_resolution, no_auto_gc from conftest import REPO @@ -193,9 +193,8 @@ def routed_restart_box(restart_box: Sandbox) -> Sandbox: box = restart_box default_resolution(box) box.env["CLAUDE_PROFILE_NAME"] = "test-profile" - for where in (("-C", str(box.wip)), ("--git-dir", str(box.origin))): - box.git(*where, "config", "gc.auto", "0") - box.git(*where, "config", "maintenance.auto", "false") + no_auto_gc(box, "-C", str(box.wip)) + no_auto_gc(box, "--git-dir", str(box.origin)) return box @@ -536,13 +535,15 @@ def test_ctx_round_trip_through_lane_checkout(routed_restart_box): result = supervisor(box, "--restart", "--lane", LANE, "--dir", str(box.lane_dir), "clear") assert result.returncode == 0, (result.stdout, result.stderr) respawn = box.tmux_log.read_text() - assert "respawn-pane" in respawn and "LANES_ROUTE_WRITES=1 " in respawn, respawn + assert "respawn-pane" in respawn, respawn + assert "LANES_ROUTE_WRITES=1 " in respawn, respawn paused = source.read_bytes() assert b"## RESUME PROMPT \xe2\x80\x94 PAUSED " in paused, paused assert paused.endswith(b"\n---\n\n# restart handoff\n\nPreserved task.\n---\nHistory.\n"), paused assert published(box, rel) == paused, "the PAUSED block was committed and pushed from the checkout" record = intent_record(box) - assert record["state"] == "pending" and record["handoff"] == str(source), record + assert record["state"] == "pending", record + assert record["handoff"] == str(source), record assert record["digest"] == hashlib.sha256(paused).hexdigest(), record operation = record["operation"] assert operation != "op-test" @@ -555,18 +556,21 @@ def test_ctx_round_trip_through_lane_checkout(routed_restart_box): capture_output=True, text=True, timeout=120, check=False) assert refused.returncode == 2, (refused.stdout, refused.stderr) assert "different handoffs" in refused.stderr or "row names is missing" in refused.stderr, refused.stderr - assert intent_path(box).read_bytes() == pending and source.read_bytes() == paused + assert intent_path(box).read_bytes() == pending + assert source.read_bytes() == paused assert box.claude_runs() == "" launched = box.start("--fresh", "--operation", operation) assert launched.returncode == 0, (launched.stdout, launched.stderr) assert "RESUME PROMPT — PAUSED " in box.claude_runs() stamped = source.read_bytes() - assert stamped.count(b"RESUMED by") == 1 and b"## RESUME PROMPT \xe2\x80\x94 PAUSED " in stamped, stamped + assert stamped.count(b"RESUMED by") == 1, stamped + assert b"## RESUME PROMPT \xe2\x80\x94 PAUSED " in stamped, stamped assert stamped.endswith(b"\n---\n\n# restart handoff\n\nPreserved task.\n---\nHistory.\n"), stamped assert published(box, rel) == stamped, "the stamp was committed and pushed from the checkout" record = intent_record(box) - assert record["state"] == "starting" and record["handoff"] == str(source), record + assert record["state"] == "starting", record + assert record["handoff"] == str(source), record assert record["digest"] == hashlib.sha256(stamped).hexdigest(), record assert_W_moved_only_by_fast_forward(box, w_before) From 439699fe2be20f0b09e7e1a66e1205626b29517f Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Sun, 8 Nov 2026 15:26:10 +0000 Subject: [PATCH 14/19] lane-handoff's register PAUSED line takes slice 3's record phrases word for word: a 0 is "handoff refreshed", LANES_NO_GIT=1 included, a 2 or 6 that wrote is "NOT published", and a 3 or any other exit after a write is "its publication NOT confirmed" (opensoft/openRepoTools#198, slice 2b) Lane openRepoTools-3's answer 6 on opensoft/openRepoTools#311 asked for one set of record phrases for the act's two writers, "refined on both sides". Slice 3 took it on the skill's side (its F23 and F24, at 2a420e0 on the code leg), and this commit makes lane-handoff's 4(b) line read the same: - a 0 records "handoff refreshed", which claims no commit. Under LANES_NO_GIT=1 that is now the record too, where item 8 wrote "handoff written, NOT published (LANES_NO_GIT=1)": the step line already says that nothing was committed (728), and the skill keeps the 0 record for every 0 (F24, which answers INFO 990 for both writers); - a 2 or a 6 that wrote: "handoff written, NOT published (write-handoff exited )"; - a 3, or any other exit after a write: "handoff written, its publication NOT confirmed (write-handoff exited )", since such an exit claims nothing either way (item 8 said "NOT published" for the others); - "handoff NOT refreshed ()" where nothing was written, unchanged. The comment over the record names the set. Item 8's explicit 3 arm assignment goes, since 3 now takes the default. Cases: test_register_paused_line_says_what_the_write_did gains a row for an exit 5 after a write, and test_no_git_handoff_says_written_and_ nothing_committed asserts "; handoff refreshed"; both red at the parent. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 --- lane-handoff | 30 ++++++++++++++++-------------- tests/test_a23_lane_handoff.py | 13 +++++++++---- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/lane-handoff b/lane-handoff index e98ffd5..0df0d4b 100755 --- a/lane-handoff +++ b/lane-handoff @@ -2651,12 +2651,14 @@ subject_text() { # handoff_written=0 # WHAT THIS ACT DID, AS THE REGISTER'S FILE-LEVEL PAUSED LINE SAYS IT (4(b) # below; #303's fit (c) in lane openRepoTools-3's review of #316, and its -# question 6 on #311). `handoff refreshed` only where `write-handoff` wrote the -# block and exited 0 with git allowed. Otherwise the skill's phrases, so the -# two writers of the one act (Amendment 17(a)) say the same thing: written -# and NOT published (2, 6, any other code that wrote, and LANES_NO_GIT=1), -# written and its publication NOT confirmed (3 only), or NOT refreshed and -# why. No reader parses it; it never claims what was not done. +# question 6 on #311). The skill's phrases, word for word (slice 3's F23 and +# F24), so the two writers of the one act (Amendment 17(a)) say the same +# thing: `handoff refreshed` where `write-handoff` wrote the block and exited +# 0, which claims no commit (under LANES_NO_GIT=1 the step line says nothing +# was committed); written and NOT published (2, 6); written and its +# publication NOT confirmed (3, and any other exit after a write, which +# claims nothing either way); or NOT refreshed and why. No reader parses it; +# it never claims what was not done. handoff_record="handoff NOT refreshed (write-handoff was not called)" if [ -z "$handoff_file" ]; then handoff_record="handoff NOT refreshed (no handoff found)" @@ -2695,29 +2697,29 @@ else # its read-back name this path, as the helper resolved it. handoff_file="${hs_line%% *}" handoff_written=1 - handoff_record="handoff written, NOT published (write-handoff exited $hs_rc)" + handoff_record="handoff written, its publication NOT confirmed (write-handoff exited $hs_rc)" step "handoff refreshed: $handoff_file (+$hs_added lines at the head)" case "$hs_rc" in # THE WORDS SAY WHAT WAS DONE (lane openRepoTools-3's review of # #316, 728 and its question 4): under the harness's # LANES_NO_GIT=1 the helper writes the file, commits nothing and # exits 0. - 0) if [ "${LANES_NO_GIT:-0}" = 1 ]; then - handoff_record="handoff written, NOT published (LANES_NO_GIT=1)" + 0) handoff_record="handoff refreshed" + if [ "${LANES_NO_GIT:-0}" = 1 ]; then step "handoff written through \`write-handoff\`, which exited 0 under LANES_NO_GIT=1: the block is written in $handoff_file and nothing is committed or pushed" else - handoff_record="handoff refreshed" step "handoff written through \`write-handoff\`, which exited 0: its words above say what it committed and pushed" fi ;; - 2) note "the handoff is written in $handoff_file and NOT committed (\`write-handoff\` exited 2: the checkout refused, and its words above say why and give the commit to make once that is answered). Never re-run this handoff to commit it: a second run puts a second block on top." ;; - 3) handoff_record="handoff written, its publication NOT confirmed (write-handoff exited 3)" - note "the handoff is written in $handoff_file and its commit pushed, and origin did not confirm it (\`write-handoff\` exited 3; its words above name the commit): look before writing it again, because a second block on top is a second handoff in the one document this protocol never rewrites." ;; + 2) handoff_record="handoff written, NOT published (write-handoff exited 2)" + note "the handoff is written in $handoff_file and NOT committed (\`write-handoff\` exited 2: the checkout refused, and its words above say why and give the commit to make once that is answered). Never re-run this handoff to commit it: a second run puts a second block on top." ;; + 3) note "the handoff is written in $handoff_file and its commit pushed, and origin did not confirm it (\`write-handoff\` exited 3; its words above name the commit): look before writing it again, because a second block on top is a second handoff in the one document this protocol never rewrites." ;; # 6 IS NOT ONE STATE (lane openRepoTools-3's review of #311, 982, # which names this line): `git add` or `commit` failed and nothing is # committed, or the commit is kept on HEAD and named; and with slice # 2a's 604, an 8(d) handoff's commit or push in its own repository # failed. The helper's words say which, so these do not guess. - 6) note "the handoff is written in $handoff_file and NOT published (\`write-handoff\` exited 6; its words above say whether a commit was made, where it is kept, and the command that finishes it). A commit kept unpushed in this lane's register checkout is published by no later write, and each write there refuses until it is (Amendment 23 S3): once those words are answered, LANES_LANE=$lane $LANES_EDIT commit \"\", the publish act." ;; + 6) handoff_record="handoff written, NOT published (write-handoff exited 6)" + note "the handoff is written in $handoff_file and NOT published (\`write-handoff\` exited 6; its words above say whether a commit was made, where it is kept, and the command that finishes it). A commit kept unpushed in this lane's register checkout is published by no later write, and each write there refuses until it is (Amendment 23 S3): once those words are answered, LANES_LANE=$lane $LANES_EDIT commit \"\", the publish act." ;; *) note "the handoff is written in $handoff_file and was NOT published (\`write-handoff\` exited $hs_rc; its words above say what is left)." ;; esac else diff --git a/tests/test_a23_lane_handoff.py b/tests/test_a23_lane_handoff.py index 14afffb..ac76f1d 100644 --- a/tests/test_a23_lane_handoff.py +++ b/tests/test_a23_lane_handoff.py @@ -181,11 +181,13 @@ def test_no_git_handoff_says_written_and_nothing_committed(tmp_path, short_root) said = [line for line in result.stderr.splitlines() if "LANES_NO_GIT=1" in line and "nothing is committed" in line] assert len(said) == 1, result.stderr - # 990 of the review of #311, on 2b: the register's PAUSED line says so too. + # 990 of the review of #311, as slice 3's F24 answers it for both writers + # of the act: a 0 records `handoff refreshed`, which claims no commit, and + # the step line above is what says that nothing was committed. register = (checkout / "lanes" / "LANES.md").read_text(encoding="utf-8") paused = [line for line in register.splitlines() if f"PAUSED — lane {LANE}, session " in line] assert len(paused) == 1, register - assert paused[0].endswith("; handoff written, NOT published (LANES_NO_GIT=1)"), paused + assert paused[0].endswith("; handoff refreshed"), paused def _forcing(box: Sandbox, rc: int, write: bool) -> Path: @@ -207,14 +209,17 @@ def _forcing(box: Sandbox, rc: int, write: bool) -> Path: (2, True, "handoff written, NOT published (write-handoff exited 2)"), (3, True, "handoff written, its publication NOT confirmed (write-handoff exited 3)"), (6, True, "handoff written, NOT published (write-handoff exited 6)"), + (5, True, "handoff written, its publication NOT confirmed (write-handoff exited 5)"), (1, False, "handoff NOT refreshed (write-handoff exited 1 and wrote nothing)"), ]) def test_register_paused_line_says_what_the_write_did(tmp_path, short_root, rc, write, record): """#303's fit (c) in lane openRepoTools-3's review of #316, and its question 6 on #311: the register's file-level PAUSED line says `handoff refreshed` only where `write-handoff` wrote the block and exited - 0. Otherwise it uses the skill's phrases: written and NOT published (2, - 6), its publication NOT confirmed (3 only), or NOT refreshed and why. And + 0. Otherwise it uses the skill's phrases (slice 3's F23): written and NOT + published (2, 6), its publication NOT confirmed (3, and any other exit + after a write, which claims nothing either way), or NOT refreshed and + why. And 982: exit 6's words no longer say the commit is kept on HEAD of the lane's checkout, since a 6 may have made no commit, or (2a's 604) made it in an 8(d) handoff's own repository.""" From e5c3326fd7e8ab339b4bbe22509db2f908662055 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Sun, 8 Nov 2026 15:34:37 +0000 Subject: [PATCH 15/19] The slice's tests pin fourteen of the mutant battery's survivors: the refreshes' scope, a refresh that could not fast-forward W, the stamp's LANES_LANE, both stale-row recoveries, the checkout made at the row, and lane-handoff's exit words, its printed block and a handoff-path that failed (opensoft/openRepoTools#198, slice 2b) m2b's battery of 59 one-line mutants of 2b's own code left 26 alive on 10-23. Earlier items pinned I1a, I2a and I2b (items 1 and 2), LS49 (item 4) and LS45 (item 8); LS46, LS47 and LH04 are part 2's 733 cases. This commit adds a case, or an assertion in one, for each of these, and each is shown killing its mutant: - LS03 and LS02: a recording LANES_EDIT shows no refresh-workspace without the opt-in, and none for a caller's LANES_REPO under it; - LS06: a normal routed start says nothing of a refresh that failed and reads the row from W's working tree; - LS07, LS10 and LS12: where W holds a commit of its own, the refresh cannot fast-forward it; the launch says so, reads the row a peer pushed from origin/main as W last fetched it, and offers no second row; - LS29: a first start says nothing of a checkout that could not be made; - LS41: the stamp's write-handoff carries LANES_LANE; - LS17: without the opt-in, a start behind a peer's row names refresh-workspace, never a fetch and merge typed in W; - LS19: under the opt-in, a row only W's working tree carries is named a stray in W; - LH13: lane-handoff's exit-0 step line; LH14: exit 2's own note ("Never re-run this handoff"); LH16: the block printed where nothing was written; LH03: a handoff-path that failed is said, the block printed, and the register records "handoff NOT refreshed (handoff-path exited 1)". LS12 and LH13 are rewritten against today's code (LS12's recorded text did not match, LH13's arm changed in item 2). Not pinned, and named in the PR body: LS08, LS21, LH07 and LH11 (equivalent today, or needing restart plumbing), LS52 (its fallback is gone since item 1) and LS20 (the shell suite's :3189, CI only). No behaviour changes. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 --- tests/test_a23_lane_handoff.py | 29 +++++++ tests/test_a23_lane_start.py | 142 +++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) diff --git a/tests/test_a23_lane_handoff.py b/tests/test_a23_lane_handoff.py index ac76f1d..f31ecad 100644 --- a/tests/test_a23_lane_handoff.py +++ b/tests/test_a23_lane_handoff.py @@ -88,6 +88,7 @@ def test_handoff_written_and_pushed_from_checkout(tmp_path, short_root): assert f"Lane: {LANE}" in _origin(box, "log", "-1", "--format=%b", "main", "--", cell) assert f"handoff refreshed: {checkout}/{cell} " in result.stderr, result.stderr assert f"committed: {SUBJECT}" in result.stderr, "write-handoff's own words are printed" + assert "which exited 0: its words above say what it committed and pushed" in result.stderr, result.stderr # THE ONE WRITE IN W IS GONE: W moved only by fast-forward, and its copy of # the handoff took no byte from this act. @@ -130,6 +131,7 @@ def test_S3_refusal_writes_file_no_commit_prints_git_words(tmp_path, short_root) assert f"commit -m \"{SUBJECT}\"" in result.stderr, result.stderr assert "NOT committed" in result.stderr, result.stderr assert "write-handoff` exited 2" in result.stderr, result.stderr + assert "Never re-run this handoff" in result.stderr, result.stderr assert_W_moved_only_by_fast_forward(box, before) assert (box.wip / cell).read_text(encoding="utf-8") == HANDOFF, "W's copy took no byte" @@ -230,6 +232,33 @@ def test_register_paused_line_says_what_the_write_did(tmp_path, short_root, rc, paused = [line for line in register.splitlines() if f"PAUSED — lane {LANE}, session " in line] assert len(paused) == 1, (register, result.stderr) assert paused[0].endswith(f"; {record}"), (paused, result.stderr) + if not write: + assert "## RESUME PROMPT — PAUSED " in result.stderr, result.stderr if rc == 6: assert "KEPT on HEAD" not in result.stderr, result.stderr assert "its words above say whether a commit was made, where it is kept" in result.stderr, result.stderr + + +def test_a_handoff_path_that_failed_is_said_and_the_block_printed(tmp_path, short_root): + """Battery survivor LH03 (decision 11): where `handoff-path` fails, + lane-handoff says it could not read where the handoff is written, prints + the block for the hand, and the register's PAUSED line says the handoff + was NOT refreshed and why (fit (c)).""" + box, _checkout, cell, _before = _ready(tmp_path, short_root) + real = box.bin / "lanes-edit.sh" + wrapper = box.root / "no-path-lanes-edit.sh" + wrapper.write_text(f"""#!/usr/bin/env bash +if [ "${{1-}}" = handoff-path ]; then echo "handoff-path: the register could not be read" >&2; exit 1; fi +exec '{real}' "$@" +""", encoding="utf-8") + wrapper.chmod(0o755) + handoff = _published(box, cell) + + result = _handoff(box, env={"LANES_EDIT": str(wrapper)}) + assert f"where lane {LANE}'s handoff is written could not be read" in result.stderr, result.stderr + assert "## RESUME PROMPT — PAUSED " in result.stderr, result.stderr + assert _published(box, cell) == handoff, "the handoff took no block" + paused = [line for line in _published(box, "lanes/LANES.md").splitlines() + if f"PAUSED — lane {LANE}, session " in line] + assert len(paused) == 1, (paused, result.stderr) + assert paused[0].endswith("; handoff NOT refreshed (handoff-path exited 1)"), paused diff --git a/tests/test_a23_lane_start.py b/tests/test_a23_lane_start.py index d0b9113..355126b 100644 --- a/tests/test_a23_lane_start.py +++ b/tests/test_a23_lane_start.py @@ -104,6 +104,7 @@ def _started(tmp_path: Path, short_root: Path) -> tuple[Sandbox, Path, Path, lis first = box.start("--dir", str(lane_dir), window_name="claude") assert first.returncode == 0, first.stderr assert (checkout / ".git").is_dir(), "the first start made the lane's checkout" + assert "own register checkout could not be made" not in first.stderr, first.stderr return box, checkout, lane_dir, before @@ -121,6 +122,8 @@ def test_launch_creates_checkout_and_writes_only_there(tmp_path, short_root): second = box.start("--fresh", window_name=LANE) assert second.returncode == 0, second.stderr + assert "W was not fast-forwarded" not in second.stderr, second.stderr + assert "as W last fetched it" not in second.stderr, second.stderr register = _published(box, "lanes/LANES.md") assert sum(1 for r in register.splitlines() if r.startswith(f"| `{LANE}`")) == 1 @@ -512,3 +515,142 @@ def test_first_prompt_follows_a_handoff_a_peer_moved_during_the_stamp(tmp_path, assert "launched with no first prompt" not in start.stderr, start.stderr assert "RESUMED by " in start.stdout, (start.stdout, start.stderr) assert "own words" in start.stdout, start.stdout + + +# ------------------------------------------- the mutant battery's survivors (item 10) + +def _peer_start_row() -> str: + """Another workstation's start of this lane: its row, naming its handoff.""" + return (f"| `{LANE}` | harness `{PEER_SID}` (minted by lane-start, 2026-10-22T00:00:00Z) | " + f"Eagle / default / peer | 2026-10-22T00:00Z | none | handoffs/repoZ/peer.md | " + f"LIVE · 2026-10-22T00:00:00Z · STARTED by {PEER_SID} (lane {LANE}) — lane-start on Eagle: row created |") + + +def _recording(box: Sandbox) -> tuple[Path, Path]: + """A `LANES_EDIT` that records each call's verb and `LANES_LANE`, then runs + the real helper.""" + calls = box.root / "lanes-edit-calls.log" + wrapper = box.root / "recording-lanes-edit.sh" + wrapper.write_text(f"""#!/usr/bin/env bash +printf '%s LANES_LANE=%s\\n' "${{1-}}" "${{LANES_LANE-}}" >> '{calls}' +exec '{box.bin / "lanes-edit.sh"}' "$@" +""", encoding="utf-8") + wrapper.chmod(0o755) + return wrapper, calls + + +def _verbs(calls: Path, verb: str) -> list[str]: + return [line for line in calls.read_text(encoding="utf-8").splitlines() if line.startswith(verb + " ")] + + +def test_no_refresh_of_W_without_the_opt_in(tmp_path, short_root): + """Battery survivor LS03 (slice 2b's decision 2): without the opt-in W is + the write checkout, which every write's S3 fast-forwards under its own + lock, so the start asks for no `refresh-workspace`.""" + box = Sandbox(tmp_path) + default_resolution(box, route=False) + lane_dir = _checkout_of_length(short_root, None) + box.checkout(lane_dir) + wrapper, calls = _recording(box) + box.env["LANES_EDIT"] = str(wrapper) + + first = box.start("--dir", str(lane_dir), window_name="claude") + assert first.returncode == 0, first.stderr + assert _verbs(calls, "canon-lane"), "the recorder saw the start's calls" + assert _verbs(calls, "refresh-workspace") == [], calls.read_text(encoding="utf-8") + + +def test_no_refresh_of_W_for_a_callers_LANES_REPO(tmp_path, short_root): + """Battery survivor LS02 (decision 2): a caller's explicit `LANES_REPO` and + `LANES_FILE` are the caller's register, not W, so even under the opt-in + the start asks for no `refresh-workspace`.""" + box = Sandbox(tmp_path) # LANES_REPO and LANES_FILE name box.wip + box.env["LANES_ROUTE_WRITES"] = "1" + lane_dir = _checkout_of_length(short_root, None) + box.checkout(lane_dir) + wrapper, calls = _recording(box) + box.env["LANES_EDIT"] = str(wrapper) + + first = box.start("--dir", str(lane_dir), window_name="claude") + assert first.returncode == 0, first.stderr + assert _verbs(calls, "canon-lane"), "the recorder saw the start's calls" + assert _verbs(calls, "refresh-workspace") == [], calls.read_text(encoding="utf-8") + + +def test_a_refresh_that_could_not_fast_forward_reads_the_row_from_origin(tmp_path, short_root): + """Battery survivors LS07, LS10 and LS12 (decisions 2 and 3): where + `refresh-workspace` cannot fast-forward W, here because W holds a commit + of its own, the launch says so and goes on, and it reads the lane's row + from origin/main as W last fetched it, never from W's working tree, which + is behind: the row a peer pushed is found, and no second row is offered.""" + box, _checkout, lane_dir = _boxed(tmp_path, short_root) + made = _edit(box, "lane-checkout", LANE, "--create") + assert made.returncode == 0, made.stderr + (box.wip / "local-note.md").write_text("a commit only W has\n", encoding="utf-8") + box.git("-C", str(box.wip), "add", "--", "local-note.md") + box.git("-C", str(box.wip), "commit", "-q", "-m", "a commit only W has") + _peer_push(box, {"lanes/LANES.md": _published(box, "lanes/LANES.md") + _peer_start_row() + "\n", + "handoffs/repoZ/peer.md": HANDOFF}, "a peer's start of the lane") + + start = box.start("--dir", str(lane_dir), "--fresh", window_name=LANE) + assert start.returncode == 0, start.stderr + assert "W was not fast-forwarded before this launch reads it" in start.stderr, start.stderr + assert "row is read from origin/main as W last fetched it" in start.stderr, start.stderr + assert "ALREADY has a row" not in start.stderr, start.stderr + assert rows(box.wip / "lanes" / "LANES.md") == [], "W's working tree is still behind" + published = [r for r in _published(box, "lanes/LANES.md").splitlines() if r.startswith(f"| `{LANE}`")] + assert len(published) == 1, published + + +def test_the_stamp_is_written_with_LANES_LANE(tmp_path, short_root): + """Battery survivor LS41 (decision 5): the stamp's `write-handoff` carries + `LANES_LANE`, as every write of a lane does (Amendment 23 S1). The helper + also routes this verb by its lane argument today, so only the call shows + it.""" + box, _checkout, _lane_dir, _before = _started(tmp_path, short_root) + cell = _cell(box) + _peer_push(box, {cell: HANDOFF}, "the lane's handoff, written elsewhere") + wrapper, calls = _recording(box) + box.env["LANES_EDIT"] = str(wrapper) + + start = box.start("--fresh", window_name=LANE) + assert start.returncode == 0, start.stderr + assert _verbs(calls, "write-handoff") == [f"write-handoff LANES_LANE={LANE}"], calls.read_text(encoding="utf-8") + + +def test_unrouted_start_behind_a_peers_row_names_refresh_workspace(tmp_path, short_root): + """Battery survivor LS17 (decision 9): without the opt-in, with the default + resolution, W is the register read and nothing refreshes it first. Where + a peer has published the lane's row and W is behind, the start refuses a + second row, and its recovery is `refresh-workspace`, the one act that + moves W (only ever a fast-forward), never a fetch and merge typed in W.""" + box = Sandbox(tmp_path) + default_resolution(box, route=False) + lane_dir = _checkout_of_length(short_root, None) + box.checkout(lane_dir) + _peer_push(box, {"lanes/LANES.md": _published(box, "lanes/LANES.md") + _peer_start_row() + "\n", + "handoffs/repoZ/peer.md": HANDOFF}, "a peer's start of the lane") + tip = _tip(box) + + start = box.start("--dir", str(lane_dir), window_name="claude") + assert start.returncode == 2, start.stderr + assert "ALREADY has a row" in start.stderr, start.stderr + assert "refresh-workspace # W is only ever fast-forwarded" in start.stderr, start.stderr + assert "merge --ff-only origin/" not in start.stderr, start.stderr + assert _tip(box) == tip, "nothing was written" + + +def test_a_row_only_in_W_is_named_a_stray_there(tmp_path, short_root): + """Battery survivor LS19 (decision 9): under the opt-in, a row that W's + working tree carries and origin does not is a stray in W, the readers' + copy, where no tool writes (Amendment 23 S5). The recovery says so and + makes it a write of the lane's own checkout, never a publish from W.""" + box, _checkout, lane_dir = _boxed(tmp_path, short_root) + register = box.wip / "lanes" / "LANES.md" + register.write_text(register.read_text(encoding="utf-8") + _peer_start_row() + "\n", encoding="utf-8") + + start = box.start("--dir", str(lane_dir), window_name="claude") + assert f"this checkout has a row for {LANE} and the register on origin/main has none" in start.stderr, \ + start.stderr + assert "RECOVERY — that row is a stray in W" in start.stderr, start.stderr + assert "publish the register from here" not in start.stderr, start.stderr From 58309ba8acb7140310644c13ef7f473781bbd899 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Sun, 8 Nov 2026 16:12:29 +0000 Subject: [PATCH 16/19] A row cell naming a sibling lane's handoff, in that lane's clone of the register beside W, is neither stamped nor delivered by lane-start and takes no PAUSED block from lane-handoff, now that slice 2a refuses it in the translation all three verbs share (opensoft/openRepoTools#198, slice 2b) Lane openRepoTools-3's review of opensoft/openRepoTools#316, MUST 723. With the lane's cell naming repoZ-2's handoff in repoZ-2's own clone of the register beside W, lane-start stamped repoZ-1's RESUMED line into that clone, uncommitted, and launched with repoZ-2's top block as its first prompt; lane-handoff wrote the PAUSED block there the same way. Slice 2a's 601 (A54), merged at e12b369, puts the refusal in the translation write-handoff, handoff-path and set-restart-intent share, and item 1 (727) made 5d read only the file handoff-path names. So no 2b code changes; one case per call site pins it: - lane-start: nothing is written in the sibling's file or checkout, none of its block is delivered, and the launch goes on with no first prompt, write-handoff's words naming another checkout of the register; - lane-handoff: nothing is written there, the block is printed, and the register's PAUSED line records "handoff NOT refreshed (handoff-path exited 2)". Both cases were red at e5c3326, before the merge (the sibling's file written), and are green on it. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 --- tests/test_a23_lane_handoff.py | 27 ++++++++++++++++- tests/test_a23_lane_start.py | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/tests/test_a23_lane_handoff.py b/tests/test_a23_lane_handoff.py index f31ecad..2a4b229 100644 --- a/tests/test_a23_lane_handoff.py +++ b/tests/test_a23_lane_handoff.py @@ -35,7 +35,7 @@ import pytest from conftest import REPO -from test_a23_lane_start import (HANDOFF, _assert_nothing_unpublished, +from test_a23_lane_start import (HANDOFF, SIBLING_WORDS, _assert_nothing_unpublished, _sibling_handoff, _with_cell, _cell, _edit, _origin, _peer_push, _published, _started, _tip) from test_a23_lane_start_read import assert_W_moved_only_by_fast_forward from test_lane_start_row_stamp import LANE, Sandbox, pytestmark, short_root # noqa: F401 @@ -262,3 +262,28 @@ def test_a_handoff_path_that_failed_is_said_and_the_block_printed(tmp_path, shor if f"PAUSED — lane {LANE}, session " in line] assert len(paused) == 1, (paused, result.stderr) assert paused[0].endswith("; handoff NOT refreshed (handoff-path exited 1)"), paused + + +def test_a_cell_in_a_sibling_lanes_checkout_takes_no_block_and_the_block_is_printed(tmp_path, short_root): + """723 of lane openRepoTools-3's review of #316, at lane-handoff's call + site: the lane's row cell names lane repoZ-2's handoff in repoZ-2's own + clone of the register beside W. `handoff-path` refuses it (slice 2a's + 601), so nothing is written there, the block is printed for the hand, and + the register's PAUSED line says the handoff was NOT refreshed.""" + box, _checkout, _cell_, _before = _ready(tmp_path, short_root) + f = _sibling_handoff(box) + sib = f.parents[2] + _peer_push(box, {"lanes/LANES.md": _with_cell(box, str(f))}, "the lane's cell names repoZ-2's handoff") + text, head = f.read_text(encoding="utf-8"), box.git("-C", str(sib), "rev-parse", "HEAD") + + result = _handoff(box) + assert f.read_text(encoding="utf-8") == text, "nothing was written in the sibling's handoff" + assert SIBLING_WORDS in text + assert box.git("-C", str(sib), "status", "--porcelain") == "", "the sibling's checkout is clean" + assert box.git("-C", str(sib), "rev-parse", "HEAD") == head, "and took no commit" + assert "in another checkout of the register beside W" in result.stderr, result.stderr + assert "## RESUME PROMPT — PAUSED " in result.stderr, result.stderr + paused = [line for line in _published(box, "lanes/LANES.md").splitlines() + if f"PAUSED — lane {LANE}, session " in line] + assert len(paused) == 1, (paused, result.stderr) + assert paused[0].endswith("; handoff NOT refreshed (handoff-path exited 2)"), paused diff --git a/tests/test_a23_lane_start.py b/tests/test_a23_lane_start.py index 355126b..868f098 100644 --- a/tests/test_a23_lane_start.py +++ b/tests/test_a23_lane_start.py @@ -654,3 +654,58 @@ def test_a_row_only_in_W_is_named_a_stray_there(tmp_path, short_root): start.stderr assert "RECOVERY — that row is a stray in W" in start.stderr, start.stderr assert "publish the register from here" not in start.stderr, start.stderr + + +# ------------------------------------------------- part 2: on slice 2a's round (723) + +SIBLING_WORDS = "not this lane's words" + + +def _sibling_handoff(box: Sandbox) -> Path: + """Lane repoZ-2's own clone of the register beside W, its handoff + committed there (lane openRepoTools-3's 723 scenario).""" + sib = Path(os.path.realpath(box.wip) + "-lanes") / "repoZ-2" + box.git("clone", "-q", str(box.root / "origin.git"), str(sib)) + no_auto_gc(box, "-C", str(sib)) + f = sib / "handoffs" / "repoZ" / "two.md" + f.parent.mkdir(parents=True, exist_ok=True) + f.write_text(f"Lane: repoZ-2 — single-use resume prompt\n\n# state\n\n{SIBLING_WORDS}\n", encoding="utf-8") + box.git("-C", str(sib), "add", "--", "handoffs/repoZ/two.md") + box.git("-C", str(sib), "commit", "-q", "-m", "repoZ-2's handoff") + return f + + +def _with_cell(box: Sandbox, cell: str) -> str: + """The published register with the lane's row naming `cell` as its handoff.""" + out = [] + for line in _published(box, "lanes/LANES.md").splitlines(keepends=True): + if line.startswith(f"| `{LANE}`"): + cells = line.split("|") + cells[6] = f" {cell} " + line = "|".join(cells) + out.append(line) + return "".join(out) + + +def test_a_cell_in_a_sibling_lanes_checkout_is_neither_stamped_nor_delivered(tmp_path, short_root): + """723 of lane openRepoTools-3's review of #316, at lane-start's two call + sites: the lane's row cell names lane repoZ-2's handoff, in repoZ-2's own + clone of the register beside W. Since slice 2a's 601 (A54) `handoff-path` + and `write-handoff` refuse a handoff in another checkout of the register, + so the stamp writes nothing there, and 5d, which reads only the file + `handoff-path` names (727), delivers none of repoZ-2's block: the launch + goes on with no first prompt, and says why.""" + box, _checkout, _lane_dir, _before = _started(tmp_path, short_root) + f = _sibling_handoff(box) + sib = f.parents[2] + _peer_push(box, {"lanes/LANES.md": _with_cell(box, str(f))}, "the lane's cell names repoZ-2's handoff") + text, head = f.read_text(encoding="utf-8"), box.git("-C", str(sib), "rev-parse", "HEAD") + + start = box.start("--fresh", window_name=LANE) + assert start.returncode == 0, start.stderr + assert f.read_text(encoding="utf-8") == text, "nothing was written in the sibling's handoff" + assert box.git("-C", str(sib), "status", "--porcelain") == "", "the sibling's checkout is clean" + assert box.git("-C", str(sib), "rev-parse", "HEAD") == head, "and took no commit" + assert SIBLING_WORDS not in start.stdout, start.stdout + assert "in another checkout of the register beside W" in start.stderr, start.stderr + assert "launched with no first prompt" in start.stderr, start.stderr From 615cfddf0b1b0c3d4efa6341bd52af43c9896fa6 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Sun, 8 Nov 2026 16:12:29 +0000 Subject: [PATCH 17/19] lane-start asks whether the restart's bound handoff changed before it reads a refused stamp's 7 as lost ownership, since slice 2a now answers both restart fences with 7 (opensoft/openRepoTools#198, slice 2b) Lane openRepoTools-3's review of opensoft/openRepoTools#316, SHOULD 724. Since slice 2a's 603 (A56), merged at e12b369, write-handoff answers the shared act's two restart-fence 2s (the bound file, the pre-image) with 7. So a bound handoff a peer changed after binding came back from 5c as 7, and lane-start, which asked the 7 first, said "this restart attempt lost ownership of its handoff". 5c now asks hs_bound_unchanged first and dies "the restart handoff changed before its owned resume stamp"; a 7 with the bound file unchanged is still lost ownership. Both exit 2, and S3's 2 stays a note (test_refused_stamp_with_restart_intent_still_launches). New case, red on the merge before this commit: test_a_bound_handoff_changed_before_the_stamp_is_named_so, whose wrapper changes the bound file just before the real write-handoff. It also pins the battery's LS47 (733's M2). Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 --- lane-start | 8 +++-- tests/test_supervised_restart_regressions.py | 37 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/lane-start b/lane-start index d90c82a..d907d27 100755 --- a/lane-start +++ b/lane-start @@ -4458,10 +4458,14 @@ if [ "$lane_verb" = RESUMED ] && [ "$row_count" = 1 ] && (( stamp_ok )); then 6) note "Rule 3's stamp is written in $hs_written and NOT published (\`write-handoff\` exited 6; its words above say whether a commit was made, where it is kept, and the command that finishes it). A commit kept unpushed in this lane's register checkout is published by no later write, and each write there refuses until it is (Amendment 23 S3): once those words are answered, LANES_LANE=$LANE $LANES_EDIT commit \"\", the publish act." ;; *) note "Rule 3's stamp is written in $hs_written and was NOT published (\`write-handoff\` exited $hs_rc; its words above say what is left). The launch is not refused for it." ;; esac - elif [ "$ri_active" = 1 ] && [ "$hs_rc" = 7 ]; then - die "this restart attempt lost ownership of its handoff before the resume stamp (\`write-handoff\` exited 7; its words are above). Nothing was launched." 2 + # THE BOUND FILE IS ASKED FIRST (lane openRepoTools-3's review of #316, + # 724): since slice 2a's 603 the restart fences' 2s are 7, so a pre-image + # a peer changed after binding comes back as 7 as well, and is not lost + # ownership. Both exit 2; S3's 2 stays a note (below). elif [ "$ri_active" = 1 ] && ! hs_bound_unchanged; then die "the restart handoff changed before its owned resume stamp. Nothing was launched." 2 + elif [ "$ri_active" = 1 ] && [ "$hs_rc" = 7 ]; then + die "this restart attempt lost ownership of its handoff before the resume stamp (\`write-handoff\` exited 7; its words are above). Nothing was launched." 2 else hs_none=1 note "Rule 3's stamp was NOT written (\`write-handoff\` exited $hs_rc; its words are above), and the launch is not refused for it: the stamp is the record of this resume, not its licence." diff --git a/tests/test_supervised_restart_regressions.py b/tests/test_supervised_restart_regressions.py index 81231da..198eb04 100644 --- a/tests/test_supervised_restart_regressions.py +++ b/tests/test_supervised_restart_regressions.py @@ -518,6 +518,43 @@ def test_refused_stamp_with_restart_intent_still_launches(routed_restart_box): assert record["digest"] == hashlib.sha256(before).hexdigest(), record +def test_a_bound_handoff_changed_before_the_stamp_is_named_so(routed_restart_box): + """724 of lane openRepoTools-3's review of #316: once slice 2a answers + the restart fences' 2s with 7 (603), a bound handoff a peer changed after + binding (here a peer's push, which S3 fast-forwards in) comes back from + the stamp as 7 too. lane-start asks the bound file + first, so the refusal says the handoff changed before the stamp, not that + the attempt lost ownership. Both exit 2, and nothing is launched.""" + box = routed_restart_box + source = prepare_restart_routed(box) + rel = str(source.relative_to(lane_checkout(box))) + peer = box.root / "peer" + box.git("clone", "-q", str(box.origin), str(peer)) + real = box.bin / "lanes-edit.sh" + mark = box.root / "peer-pushed.mark" + wrapper = box.root / "peer-edits-lanes-edit.sh" + wrapper.write_text(f"""#!/usr/bin/env bash +if [ "${{1-}}" = write-handoff ] && [ ! -e '{mark}' ]; then + : > '{mark}' + git -C '{peer}' fetch -q origin main && git -C '{peer}' merge -q --ff-only origin/main && + printf 'a peer changed this after binding\\n' >> '{peer}/{rel}' && + git -C '{peer}' commit -q -am 'a peer changes the bound handoff' && + git -C '{peer}' push -q origin HEAD:main || exit 99 +fi +exec '{real}' "$@" +""", encoding="utf-8") + wrapper.chmod(0o755) + box.env["LANES_EDIT"] = str(wrapper) + + result = box.start("--fresh", "--operation", "op-test") + assert result.returncode == 2, (result.stdout, result.stderr) + # lane-start's own line: the helper's refusal says the same words of its write. + assert "lane-start: the restart handoff changed before its owned resume stamp. Nothing was launched." in result.stderr, result.stderr + assert "lost ownership of its handoff" not in result.stderr, result.stderr + assert mark.exists(), "the peer changed the bound handoff during the stamp" + assert box.claude_runs() == "" + + def test_ctx_round_trip_through_lane_checkout(routed_restart_box): """`/ctx` under the opt-in, both halves: `lane-handoff --restart` writes the PAUSED top block through the lane's checkout and binds THAT file in From 5bf9bbb97db19dbb2508a8afbe5e2a5d0a63e912 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Sun, 8 Nov 2026 16:12:29 +0000 Subject: [PATCH 18/19] An Amendment 8(d) handoff is stamped and takes the PAUSED block committed and pushed in its own repository at both call sites, and both scripts' exit-0 words point at the helper's account, which may say nothing was committed (opensoft/openRepoTools#198, slice 2b) Lane openRepoTools-3's review of opensoft/openRepoTools#316, SHOULD 722 and 721's last case, with 982's 8(d) case from its review of opensoft/openRepoTools#311. Lane 3 read Amendment 23 S2 as governing, and slice 2a's 604 (A57), merged at e12b369, commits and pushes an 8(d) handoff in its own repository, as e2037b9 did. Pinned at both call sites: the stamp and the PAUSED block are committed there by pathspec and pushed to that repository's origin, its tree clean, the register taking no commit for them; an 8(d) stamp whose push fails there is a 6 whose note points at the helper's words, never at HEAD of the lane's checkout. A 0 is still not always a commit: an 8(d) handoff in no work tree, or a clone of the register not beside W, is committed nowhere, and the helper's note says so. So both scripts' exit-0 words now end as slice 3's 0 arm does (its F24), "or that nothing was committed": - lane-start: "handoff stamped through `write-handoff`, which exited 0: : (its words above say what it committed and pushed, or that nothing was committed)", where it said "handoff stamped, committed and pushed"; - lane-handoff: "... its words above say what it committed and pushed, or that nothing was committed". The three golden lines of lane-handoff's exit-0 step (decision 22's hunks) and the shell suite's :3822 (CI only) move with them. New cases, red at e5c3326 before the merge (the 8(d) file left uncommitted), and the two exit-0 cases red on the merge before this commit's words: test_an_8d_cell_is_stamped_committed_and_pushed_in_its_ own_repository, test_an_8d_stamp_whose_push_fails_there_says_what_the_ helper_said, test_an_8d_cell_takes_the_block_committed_and_pushed_in_ its_own_repository. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 --- lane-handoff | 8 ++- lane-start | 9 ++- .../single-repository-decoys.golden | 4 +- tests/fixtures/t018/single-repository.golden | 2 +- tests/test_a23_lane_handoff.py | 36 +++++++++- tests/test_a23_lane_start.py | 72 +++++++++++++++++++ tests/test_lane_helpers.sh | 2 +- 7 files changed, 124 insertions(+), 9 deletions(-) diff --git a/lane-handoff b/lane-handoff index 0df0d4b..452c02c 100755 --- a/lane-handoff +++ b/lane-handoff @@ -2703,12 +2703,16 @@ else # THE WORDS SAY WHAT WAS DONE (lane openRepoTools-3's review of # #316, 728 and its question 4): under the harness's # LANES_NO_GIT=1 the helper writes the file, commits nothing and - # exits 0. + # exits 0. And since slice 2a's 604 a 0 may commit elsewhere or + # nowhere: an Amendment 8(d) handoff is committed and pushed in its + # own repository, and one in no work tree, or in a clone of the + # register not beside W, is committed nowhere; the helper's note says + # which (722; slice 3's F24). 0) handoff_record="handoff refreshed" if [ "${LANES_NO_GIT:-0}" = 1 ]; then step "handoff written through \`write-handoff\`, which exited 0 under LANES_NO_GIT=1: the block is written in $handoff_file and nothing is committed or pushed" else - step "handoff written through \`write-handoff\`, which exited 0: its words above say what it committed and pushed" + step "handoff written through \`write-handoff\`, which exited 0: its words above say what it committed and pushed, or that nothing was committed" fi ;; 2) handoff_record="handoff written, NOT published (write-handoff exited 2)" note "the handoff is written in $handoff_file and NOT committed (\`write-handoff\` exited 2: the checkout refused, and its words above say why and give the commit to make once that is answered). Never re-run this handoff to commit it: a second run puts a second block on top." ;; diff --git a/lane-start b/lane-start index d907d27..a011f37 100755 --- a/lane-start +++ b/lane-start @@ -4443,11 +4443,16 @@ if [ "$lane_verb" = RESUMED ] && [ "$row_count" = 1 ] && (( stamp_ok )); then case "$hs_rc" in # THE WORDS SAY WHAT WAS DONE (lane openRepoTools-3's review of #316, # 721 and its question 4): under the harness's LANES_NO_GIT=1 the - # helper writes the file and commits nothing, and exits 0. + # helper writes the file and commits nothing, and exits 0. And since + # slice 2a's 604 a 0 is not always a commit in this lane's checkout: + # an Amendment 8(d) handoff is committed and pushed in its own + # repository, and one in no work tree, or in a clone of the register + # not beside W, is committed nowhere. The helper's note says which + # (722; slice 3's F24 words the skill's 0 the same way). 0) if [ "${LANES_NO_GIT:-0}" = 1 ]; then note "Rule 3's stamp is written in $hs_written and NOT committed: LANES_NO_GIT=1 commits and pushes nothing (\`write-handoff\` exited 0): $hs_stamp" else - step "handoff stamped, committed and pushed: $hs_written: $hs_stamp" + step "handoff stamped through \`write-handoff\`, which exited 0: $hs_written: $hs_stamp (its words above say what it committed and pushed, or that nothing was committed)" fi ;; 3) note "Rule 3's stamp is written in $hs_written and its commit pushed, and origin did not confirm it (\`write-handoff\` exited 3; its words above name the commit): look before writing it again, because a stamp written twice is two resumes in the one document this protocol never rewrites." ;; # 6 IS NOT ONE STATE (lane openRepoTools-3's review of #311, 982): diff --git a/tests/fixtures/t018-handoff/single-repository-decoys.golden b/tests/fixtures/t018-handoff/single-repository-decoys.golden index 41ebe51..1a783a5 100644 --- a/tests/fixtures/t018-handoff/single-repository-decoys.golden +++ b/tests/fixtures/t018-handoff/single-repository-decoys.golden @@ -34,7 +34,7 @@ lane-handoff: writers polled: 1 under /projects/repoD lanes-edit: committed: handoff(repoD-1@Eagle): PAUSED, t018 handoff lanes-edit: pushed origin/main (attempt 1) lane-handoff: handoff refreshed: /projects/wip/handoffs/repoD-1.md (+14 lines at the head) -lane-handoff: handoff written through `write-handoff`, which exited 0: its words above say what it committed and pushed +lane-handoff: handoff written through `write-handoff`, which exited 0: its words above say what it committed and pushed, or that nothing was committed lanes-edit: 1 line appended to /projects/wip/lanes/log/repoD-1.md lanes-edit: committed: LOG(repoD-1@Eagle): PAUSED lane:repoD-1 lanes-edit: pushed origin/main (attempt 1) @@ -109,7 +109,7 @@ lane-handoff: writers polled: 1 under /projects/repoQ lanes-edit: committed: handoff(repoQ-1@Eagle): PAUSED, t018 handoff lanes-edit: pushed origin/main (attempt 1) lane-handoff: handoff refreshed: /projects/wip/handoffs/repoQ-1.md (+14 lines at the head) -lane-handoff: handoff written through `write-handoff`, which exited 0: its words above say what it committed and pushed +lane-handoff: handoff written through `write-handoff`, which exited 0: its words above say what it committed and pushed, or that nothing was committed lanes-edit: 1 line appended to /projects/wip/lanes/log/repoQ-1.md lanes-edit: committed: LOG(repoQ-1@Eagle): PAUSED lane:repoQ-1 lanes-edit: pushed origin/main (attempt 1) diff --git a/tests/fixtures/t018/single-repository.golden b/tests/fixtures/t018/single-repository.golden index 8235e63..b4f8b89 100644 --- a/tests/fixtures/t018/single-repository.golden +++ b/tests/fixtures/t018/single-repository.golden @@ -142,7 +142,7 @@ lane-handoff: writers polled: 2 under /projects/repoS lanes-edit: committed: handoff(repoS-1@Eagle): PAUSED, t018 handoff lanes-edit: pushed origin/main (attempt 1) lane-handoff: handoff refreshed: /projects/wip/handoffs/repoS-1.md (+15 lines at the head) -lane-handoff: handoff written through `write-handoff`, which exited 0: its words above say what it committed and pushed +lane-handoff: handoff written through `write-handoff`, which exited 0: its words above say what it committed and pushed, or that nothing was committed lanes-edit: 1 line appended to /projects/wip/lanes/log/repoS-1.md lanes-edit: committed: LOG(repoS-1@Eagle): PAUSED lane:repoS-1 lanes-edit: pushed origin/main (attempt 1) diff --git a/tests/test_a23_lane_handoff.py b/tests/test_a23_lane_handoff.py index 2a4b229..90b8ba9 100644 --- a/tests/test_a23_lane_handoff.py +++ b/tests/test_a23_lane_handoff.py @@ -35,7 +35,8 @@ import pytest from conftest import REPO -from test_a23_lane_start import (HANDOFF, SIBLING_WORDS, _assert_nothing_unpublished, _sibling_handoff, _with_cell, +from test_a23_lane_start import (HANDOFF, SIBLING_WORDS, _assert_nothing_unpublished, _other_repository, + _sibling_handoff, _with_cell, _cell, _edit, _origin, _peer_push, _published, _started, _tip) from test_a23_lane_start_read import assert_W_moved_only_by_fast_forward from test_lane_start_row_stamp import LANE, Sandbox, pytestmark, short_root # noqa: F401 @@ -287,3 +288,36 @@ def test_a_cell_in_a_sibling_lanes_checkout_takes_no_block_and_the_block_is_prin if f"PAUSED — lane {LANE}, session " in line] assert len(paused) == 1, (paused, result.stderr) assert paused[0].endswith("; handoff NOT refreshed (handoff-path exited 2)"), paused + + + +def test_an_8d_cell_takes_the_block_committed_and_pushed_in_its_own_repository(tmp_path, short_root): + """722 of lane openRepoTools-3's review of #316, at lane-handoff's write: + slice 2a's 604 (A57) commits and pushes an Amendment 8(d) handoff in its + own repository. The block is on top, committed by pathspec under the + PAUSED subject and pushed to that repository's origin, its tree clean; + the register's PAUSED line records `handoff refreshed`; and the exit-0 + words point at the helper's (a 0 may also commit nothing, F24).""" + box, _checkout, _cell_, _before = _ready(tmp_path, short_root) + other = _other_repository(box) + _peer_push(box, {"lanes/LANES.md": _with_cell(box, str(other / "handoff.md"))}, + "the lane's cell names another repository's handoff") + before = box.git("-C", str(other), "rev-parse", "HEAD").strip() + + result = _handoff(box) + assert result.returncode == 0, result.stderr + written = (other / "handoff.md").read_text(encoding="utf-8") + assert written.startswith(f"Lane: {LANE} (test-profile, session "), written + assert written.endswith("\n---\n\n" + HANDOFF), written + o = ["-C", str(other)] + assert box.git(*o, "status", "--porcelain") == "", "its tree is clean" + assert box.git(*o, "rev-parse", "HEAD~1").strip() == before + assert box.git(*o, "log", "-1", "--format=%s").strip() == SUBJECT + assert (box.git("--git-dir", str(box.root / "other.git"), "rev-parse", "main").strip() + == box.git(*o, "rev-parse", "HEAD").strip()), "and its origin holds the commit" + assert "committed there and pushed to origin/main" in result.stderr, result.stderr + assert "what it committed and pushed, or that nothing was committed" in result.stderr, result.stderr + paused = [line for line in _published(box, "lanes/LANES.md").splitlines() + if f"PAUSED — lane {LANE}, session " in line] + assert len(paused) == 1, (paused, result.stderr) + assert paused[0].endswith("; handoff refreshed"), paused diff --git a/tests/test_a23_lane_start.py b/tests/test_a23_lane_start.py index 868f098..f52e4fd 100644 --- a/tests/test_a23_lane_start.py +++ b/tests/test_a23_lane_start.py @@ -709,3 +709,75 @@ def test_a_cell_in_a_sibling_lanes_checkout_is_neither_stamped_nor_delivered(tmp assert SIBLING_WORDS not in start.stdout, start.stdout assert "in another checkout of the register beside W" in start.stderr, start.stderr assert "launched with no first prompt" in start.stderr, start.stderr + + + +def _other_repository(box: Sandbox, *, origin: bool = True) -> Path: + """Another repository, not the register (Amendment 8(d)): its handoff + committed on `main`, and (with `origin`) a bare origin `main` tracks.""" + other = box.root / "other" + box.git("init", "-q", "-b", "main", str(other)) + no_auto_gc(box, "-C", str(other)) + (other / "handoff.md").write_text(HANDOFF, encoding="utf-8") + box.git("-C", str(other), "add", "--", "handoff.md") + box.git("-C", str(other), "commit", "-q", "-m", "another repository's handoff") + if origin: + bare = box.root / "other.git" + box.git("init", "-q", "--bare", "-b", "main", str(bare)) + box.git("-C", str(other), "remote", "add", "origin", str(bare)) + box.git("-C", str(other), "push", "-q", "-u", "origin", "main") + return other + + +def test_an_8d_cell_is_stamped_committed_and_pushed_in_its_own_repository(tmp_path, short_root): + """722 of lane openRepoTools-3's review of #316, at lane-start's stamp: + slice 2a's 604 (A57) commits and pushes an Amendment 8(d) handoff in its + own repository, as e2037b9 did. The stamp is there, committed and pushed + to that repository's origin and its tree clean; the register's origin + takes no stamp commit; the first prompt is that file's top block; and the + exit-0 words point at the helper's, which say where it was committed (a + 0 may also commit nothing, slice 3's F24).""" + box, _checkout, _lane_dir, _before = _started(tmp_path, short_root) + other = _other_repository(box) + _peer_push(box, {"lanes/LANES.md": _with_cell(box, str(other / "handoff.md"))}, + "the lane's cell names another repository's handoff") + before = box.git("-C", str(other), "rev-parse", "HEAD").strip() + + start = box.start("--fresh", window_name=LANE) + assert start.returncode == 0, start.stderr + stamped = (other / "handoff.md").read_text(encoding="utf-8") + assert stamped.splitlines()[2].startswith("RESUMED by "), stamped + o = ["-C", str(other)] + assert box.git(*o, "status", "--porcelain") == "", "its tree is clean" + assert box.git(*o, "rev-parse", "HEAD~1").strip() == before + assert box.git(*o, "log", "-1", "--format=%s").startswith(f"handoff({LANE}@Eagle): RESUMED by ") + assert (box.git("--git-dir", str(box.root / "other.git"), "rev-parse", "main").strip() + == box.git(*o, "rev-parse", "HEAD").strip()), "and its origin holds the commit" + assert not [s for s in _origin(box, "log", "--format=%s", "main").splitlines() + if s.startswith(f"handoff({LANE}@Eagle): RESUMED by ")], "the register took no stamp commit" + assert "committed there and pushed to origin/main" in start.stderr, start.stderr + assert "handoff stamped through `write-handoff`, which exited 0: " in start.stderr, start.stderr + assert "or that nothing was committed" in start.stderr, start.stderr + assert "own words" in start.stdout, start.stdout + + +def test_an_8d_stamp_whose_push_fails_there_says_what_the_helper_said(tmp_path, short_root): + """982 of lane openRepoTools-3's review of #311 (2b's words share it), its + 8(d) case: another repository with no origin, so slice 2a's 604 commits + the stamp there and cannot push it, and `write-handoff` exits 6. The + launch goes on, and lane-start's note points at the helper's words, which + say where the commit is kept, never at HEAD of the lane's checkout.""" + box, _checkout, _lane_dir, _before = _started(tmp_path, short_root) + other = _other_repository(box, origin=False) + _peer_push(box, {"lanes/LANES.md": _with_cell(box, str(other / "handoff.md"))}, + "the lane's cell names another repository's handoff") + before = box.git("-C", str(other), "rev-parse", "HEAD").strip() + + start = box.start("--fresh", window_name=LANE) + assert start.returncode == 0, start.stderr + assert box.git("-C", str(other), "rev-parse", "HEAD~1").strip() == before, "committed there" + assert "and committed there, on main" in start.stderr, start.stderr + said = [line for line in start.stderr.splitlines() if "`write-handoff` exited 6" in line] + assert len(said) == 1, start.stderr + assert "its words above say whether a commit was made, where it is kept" in said[0], said + assert "KEPT on HEAD" not in start.stderr, start.stderr diff --git a/tests/test_lane_helpers.sh b/tests/test_lane_helpers.sh index 2bb4469..291437b 100755 --- a/tests/test_lane_helpers.sh +++ b/tests/test_lane_helpers.sh @@ -3819,7 +3819,7 @@ has "…the stamp written and committed here" "$(git -C "$WIP" log --format=%s is "…and PUSHED: there is no pull for it to need" "$(git -C "$WIP" fetch -q origin && git -C "$WIP" rev-list --count refs/remotes/origin/main..HEAD)" 0 hasnt "…no pull run or printed" "$err" "pull --rebase" hasnt "…and none of the old branch's words" "$err" "the pull before the push" -has "…the run saying where it went" "$err" "handoff stamped, committed and pushed: " +has "…the run saying where it went" "$err" "handoff stamped through \`write-handoff\`, which exited 0: " # ------------------------------------------------- lane-start --confirm / --yes # From c12f0780568666e039301ac427a064d5c282c26c Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Sun, 8 Nov 2026 16:12:29 +0000 Subject: [PATCH 19/19] The slice's restart cases pin 5c's lost-ownership die and a /ctx round trip while W is behind and cannot be fast-forwarded, and a why with a tab in lane-handoff (opensoft/openRepoTools#198, slice 2b) Lane openRepoTools-3's review of opensoft/openRepoTools#316, SHOULD 733, its remaining cases, and slice 2a's Q2 caveat: - M1 (the battery's LS46): under a restart, a stamp write-handoff refuses with 7 and no written line, the bound file unchanged, dies 2 "lost ownership of its handoff before the resume stamp", nothing launched (with slice 2a's 603 this is the fence's code, and S3's 2 stays a note); - 733's first case and 2a's Q2 caveat: with W holding a commit of its own and a peer's push since, no refresh can fast-forward W; the /ctx round trip still writes and binds the lane's checkout's file, path and digest through handoff-path, the launch says W was not fast-forwarded, delivers and stamps that file, and W's HEAD does not move; - M4 (the battery's LH04): a why with a tab refreshes the handoff, the subject carrying a space (decision 19). M2 is the case of the previous commit (724). M3 has no case of its own: handoff_written on a written but uncommitted /ctx is set from the written-file line, which S3's 2 prints, and the battery's LH12 (the same flag held at 0) is killed by the round trip. No behaviour changes; each case passed before this commit's tests were added, so each is shown by its mutant. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 --- tests/test_a23_lane_handoff.py | 19 +++++- tests/test_supervised_restart_regressions.py | 67 ++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/tests/test_a23_lane_handoff.py b/tests/test_a23_lane_handoff.py index 90b8ba9..e501ceb 100644 --- a/tests/test_a23_lane_handoff.py +++ b/tests/test_a23_lane_handoff.py @@ -45,7 +45,7 @@ SUBJECT = f"handoff({LANE}@Eagle): PAUSED, {WHY}" -def _handoff(box: Sandbox, *args: str, env: dict | None = None) -> subprocess.CompletedProcess: +def _handoff(box: Sandbox, *args: str, env: dict | None = None, why: str = WHY) -> subprocess.CompletedProcess: """`lane-handoff --lane repoZ-1 … `, from a shell with no tmux.""" if not (box.bin / "lane-handoff").exists(): shutil.copy2(REPO / "lane-handoff", box.bin / "lane-handoff") @@ -55,7 +55,7 @@ def _handoff(box: Sandbox, *args: str, env: dict | None = None) -> subprocess.Co full = {k: v for k, v in box.env.items() if not k.startswith("WORKBENCHES_")} full.update(LANE_HANDOFF_NO_TMUX="1", CLAUDE_PROFILE_NAME="test-profile") full.update(env or {}) - return subprocess.run([str(box.bin / "lane-handoff"), "--lane", LANE, *args, WHY], + return subprocess.run([str(box.bin / "lane-handoff"), "--lane", LANE, *args, why], env=full, capture_output=True, text=True, timeout=180, check=False) @@ -321,3 +321,18 @@ def test_an_8d_cell_takes_the_block_committed_and_pushed_in_its_own_repository(t if f"PAUSED — lane {LANE}, session " in line] assert len(paused) == 1, (paused, result.stderr) assert paused[0].endswith("; handoff refreshed"), paused + + + +def test_a_why_with_a_tab_is_a_space_in_the_subject(tmp_path, short_root): + """733 (M4, the battery's LH04) of lane openRepoTools-3's review of #316: + `write-handoff` refuses a subject that carries a control character, so + `subject_text` spells each one as a space, and a why with a tab still + refreshes the handoff, as the old `git commit -m` did (decision 19).""" + box, _checkout, cell, _before = _ready(tmp_path, short_root) + + result = _handoff(box, why="a test\thandoff") + assert result.returncode == 0, result.stderr + published = _published(box, cell) + assert published.endswith("\n---\n\n" + HANDOFF), published + assert _origin(box, "log", "-1", "--format=%s", "main", "--", cell).strip() == SUBJECT diff --git a/tests/test_supervised_restart_regressions.py b/tests/test_supervised_restart_regressions.py index 198eb04..2d2452c 100644 --- a/tests/test_supervised_restart_regressions.py +++ b/tests/test_supervised_restart_regressions.py @@ -555,6 +555,30 @@ def test_a_bound_handoff_changed_before_the_stamp_is_named_so(routed_restart_box assert box.claude_runs() == "" +def test_a_stamp_refused_as_lost_ownership_launches_nothing(routed_restart_box): + """733 (M1, the battery's LS46) of lane openRepoTools-3's review of #316: + under a restart, a stamp `write-handoff` refuses with 7 and no written + line, the bound file unchanged, is this attempt's lost ownership, and the + launch dies (2) saying so: nothing launched, the file as it was.""" + box = routed_restart_box + source = prepare_restart_routed(box) + before = source.read_bytes() + real = box.bin / "lanes-edit.sh" + wrapper = box.root / "seven-lanes-edit.sh" + wrapper.write_text(f"""#!/usr/bin/env bash +if [ "${{1-}}" = write-handoff ]; then echo "write-handoff: this attempt no longer owns the restart" >&2; exit 7; fi +exec '{real}' "$@" +""", encoding="utf-8") + wrapper.chmod(0o755) + box.env["LANES_EDIT"] = str(wrapper) + + result = box.start("--fresh", "--operation", "op-test") + assert result.returncode == 2, (result.stdout, result.stderr) + assert "lost ownership of its handoff before the resume stamp" in result.stderr, result.stderr + assert box.claude_runs() == "" + assert source.read_bytes() == before + + def test_ctx_round_trip_through_lane_checkout(routed_restart_box): """`/ctx` under the opt-in, both halves: `lane-handoff --restart` writes the PAUSED top block through the lane's checkout and binds THAT file in @@ -612,6 +636,49 @@ def test_ctx_round_trip_through_lane_checkout(routed_restart_box): assert_W_moved_only_by_fast_forward(box, w_before) +def test_ctx_round_trip_with_W_behind_and_its_refresh_blocked(routed_restart_box): + """733's first case of lane openRepoTools-3's review of #316, and slice + 2a's Q2 caveat (bind the path and the digest through `handoff-path`, + with W behind): W holds a commit of its own and a peer has pushed since, + so no refresh can fast-forward W. `lane-handoff --restart` still writes + and binds the lane's checkout's file, path and digest; the `lane-start` + the respawn runs says W was not fast-forwarded, delivers that block and + stamps it there; and W's HEAD does not move.""" + box = routed_restart_box + source = prepare_restart_routed(box) + checkout = lane_checkout(box) + rel = str(source.relative_to(checkout)) + assert helper(box, "set-restart-intent", LANE, "ready", "--expect", "pending").returncode == 0 + (box.wip / "local-note.md").write_text("a commit only W has\n", encoding="utf-8") + box.git("-C", str(box.wip), "add", "--", "local-note.md") + box.git("-C", str(box.wip), "commit", "-qm", "a commit only W has") + peer = box.root / "peer" + box.git("clone", "-q", str(box.origin), str(peer)) + (peer / "peer-note.md").write_text("a peer's commit\n", encoding="utf-8") + box.git("-C", str(peer), "add", "--", "peer-note.md") + box.git("-C", str(peer), "commit", "-qm", "a peer's commit") + box.git("-C", str(peer), "push", "-q", "origin", "HEAD:main") + w_head = box.git("-C", str(box.wip), "rev-parse", "HEAD") + + result = supervisor(box, "--restart", "--lane", LANE, "--dir", str(box.lane_dir), "clear") + assert result.returncode == 0, (result.stdout, result.stderr) + paused = source.read_bytes() + assert b"## RESUME PROMPT \xe2\x80\x94 PAUSED " in paused, paused + assert published(box, rel) == paused, "the PAUSED block was committed and pushed from the checkout" + record = intent_record(box) + assert record["handoff"] == str(source), record + assert record["digest"] == hashlib.sha256(paused).hexdigest(), record + + launched = box.start("--fresh", "--operation", record["operation"]) + assert launched.returncode == 0, (launched.stdout, launched.stderr) + assert "W was not fast-forwarded" in launched.stderr, launched.stderr + assert "RESUME PROMPT — PAUSED " in box.claude_runs() + stamped = source.read_bytes() + assert stamped.count(b"RESUMED by") == 1, stamped + assert published(box, rel) == stamped, "the stamp was committed and pushed from the checkout" + assert box.git("-C", str(box.wip), "rev-parse", "HEAD") == w_head, "W's HEAD stayed put" + + def test_restart_historical_ready_intent_does_not_control_ordinary_resume(restart_box): box = restart_box prepare_restart(box)