diff --git a/lane-handoff b/lane-handoff index e1a43ce..452c02c 100755 --- a/lane-handoff +++ b/lane-handoff @@ -108,6 +108,13 @@ # 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_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) @@ -2509,28 +2516,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,89 +2616,120 @@ 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 +# 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). 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)" + [ "$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 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 + 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. 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, 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." ;; + 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) 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 - 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" + 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 fi fi @@ -2887,7 +2932,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_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 " @@ -3125,6 +3170,20 @@ 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. + # 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/lane-start b/lane-start index 3723114..a011f37 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 # @@ -1539,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="" @@ -1569,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" ;; @@ -1621,24 +1664,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" || :)" @@ -1659,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 @@ -1689,7 +1763,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 +1792,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 +2647,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" ;; @@ -3177,59 +3269,24 @@ 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. -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=$? 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 @@ -3346,9 +3403,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 +3908,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 +4156,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), and a first prompt is read from that file alone, never from W's copy (5d)" + 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 +4360,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 +4404,84 @@ 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" ] +} +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" 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%% *}" + # 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 + 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. 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 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): + # `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 + # 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." 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" + # 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 @@ -4551,8 +4514,39 @@ if (( first_prompt )); then else fp_cell="$handoff" fi + # 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="" - [ -n "$fp_cell" ] && fp_file="$(hs_resolve "$fp_cell" 2>/dev/null || :)" + 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 @@ -4575,8 +4569,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 @@ -4682,6 +4689,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/fixtures/t018-handoff/single-repository-decoys.golden b/tests/fixtures/t018-handoff/single-repository-decoys.golden index e547835..1a783a5 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, 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) @@ -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, 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 1d02e80..b4f8b89 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, 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 new file mode 100644 index 0000000..e501ceb --- /dev/null +++ b/tests/test_a23_lane_handoff.py @@ -0,0 +1,338 @@ +# 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 + +import pytest + +from conftest import REPO +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 + +WHY = "a test handoff" +SUBJECT = f"handoff({LANE}@Eagle): PAUSED, {WHY}" + + +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") + (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, published + assert rest == HANDOFF, published + assert block.startswith(f"Lane: {LANE} (test-profile, session "), 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 + 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. + 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) + + +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, 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" + + +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, 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) + + +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 + # 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 refreshed"), 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)"), + (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 (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.""" + 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 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 + + +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 + + + +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 + + + +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_a23_lane_start.py b/tests/test_a23_lane_start.py new file mode 100644 index 0000000..f52e4fd --- /dev/null +++ b/tests/test_a23_lane_start.py @@ -0,0 +1,783 @@ +# 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 (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" +PEER_SID = "bbbb0001-2222-4000-8000-bbbb00012222" + + +# ------------------------------------------------------------------ helpers + +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)) + 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 + + +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 _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) + 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 + + +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 = 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" + assert "own register checkout could not be made" not in first.stderr, first.stderr + return box, checkout, lane_dir, before + + +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 + 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 + row = [r for r in register.splitlines() if r.startswith(f"| `{LANE}`")][0] + 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, 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_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) + + +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 = 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) | " + 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_moved_only_by_fast_forward(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, 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) + _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, 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" + + +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 = W_reflog(box) + + plan = box.start("--fresh", "--dry-run", window_name=LANE) + assert plan.returncode == 0, plan.stderr + 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 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) + + +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 + 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) + + +# ------------------------------------------- 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) + 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") + 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 + + +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 + + +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 + + +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" + + +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 + + +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 + + +# ------------------------------------------- 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 + + +# ------------------------------------------------- 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 + + + +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_a23_lane_start_read.py b/tests/test_a23_lane_start_read.py index ab14fd4..fdb6dba 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,53 @@ 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() + + +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 +110,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 +162,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 +181,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 +294,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_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 56e0bce..d443c0f 100755 --- a/tests/test_lane_helpers.sh +++ b/tests/test_lane_helpers.sh @@ -3208,7 +3208,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 @@ -3724,24 +3724,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; @@ -3761,14 +3757,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" @@ -3783,26 +3773,23 @@ 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 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). 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 @@ -3821,23 +3808,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" @@ -3848,24 +3836,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 through \`write-handoff\`, which exited 0: " # ------------------------------------------------- lane-start --confirm / --yes # 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_repo_hygiene.py b/tests/test_repo_hygiene.py index 7f238f8..d5e39e6 100644 --- a/tests/test_repo_hygiene.py +++ b/tests/test_repo_hygiene.py @@ -3289,9 +3289,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; 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` 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)") @@ -3396,6 +3398,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), diff --git a/tests/test_supervised_restart_regressions.py b/tests/test_supervised_restart_regressions.py index 0f4cc84..2d2452c 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, no_auto_gc 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,59 @@ 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" + no_auto_gc(box, "-C", str(box.wip)) + no_auto_gc(box, "--git-dir", str(box.origin)) + 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 +454,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 +468,215 @@ 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 + # 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" + 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_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_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 + 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, 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", record + assert 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 + 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, 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", 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) + + +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): @@ -744,6 +1007,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) @@ -1064,6 +1386,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" 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)