Skip to content
17 changes: 15 additions & 2 deletions docs/README-lanes.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,20 @@ creating a second one; two rows differing only by case are a refusal naming both

**On a rebase conflict it aborts**, leaves the worktree clean and not
mid-rebase, prints the conflicting lines and prints the recovery commands. Your
edit survives as a local commit; read it back with
edit survives as a local commit — but an aborted pull is not proof it never
reached origin: this checkout is shared with every lane on the workstation, so
the very next write out of it can pull this dangling commit onto its own and
push both together (#32). LOOK first: `git -C <the workspace checkout> fetch
origin main` — if THAT fails, STOP and retry it; a stale or unreachable origin
proves nothing either way, and is not permission to reset and redo. Only once
the fetch succeeds do you check by CONTENT whether your own commit is already
there, never by sha: the very peer write this recovery exists for carries a
dangling commit by rebasing it onto a newer base, which changes its hash, so
`merge-base --is-ancestor` alone can still say "not there" once the change is
actually published. `git -C <the workspace checkout> cherry origin/main <your
commit> | grep -c '^+'` compares by patch instead and still finds it; 0 means
it is already there, in which case STOP. Only once fetch succeeded AND this
prints something other than 0 do you read your edit back with
`git -C <the workspace checkout> diff origin/main..HEAD -- lanes/LANES.md`,
then `git -C <the workspace checkout> reset --hard origin/main` and redo it on
top of the peer's version.
Expand Down Expand Up @@ -936,7 +949,7 @@ else.
| 0 | done |
| 1 | environment — no register, no writer |
| 2 | refusal — bad arguments, the object is held, an unknown alias, a checkout that cannot be rebased |
| 3 | rebase conflict — nothing pushed, the edit is a local commit |
| 3 | the edit is never a permanent loss, but this attempt cannot confirm whether it also reached origin — a rebase conflict (not pushed by this attempt; a later write from this checkout may already carry it to origin, so look by content before you retry), a network timeout on a push or a pull (a hung one often already landed), or six attempts exhausted because a peer's uncommitted file blocks every rebase |
| 4 | the mutex could not be taken within 60s |
| 5 | an edit moved more than one line and was refused |
| 6 | `git add` / `commit` / `push` failed |
Expand Down
51 changes: 47 additions & 4 deletions lanes-edit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,17 @@
# carry both without the caller guessing, so the newer reads spend a number
# of their own and the older half of this file keeps 2 where it always was.
# Clause (h)'s table is the contract for which read uses which.
# 3 rebase conflict — nothing was pushed, the edit is a local commit
# 3 the edit is never a permanent loss, but this attempt cannot confirm
# whether it also reached origin. THREE CAUSES (Copilot round 3 on #50,
# 5203261904, naming the other two this row used to leave out): a
# rebase conflict this attempt could not resolve — not pushed BY THIS
# ATTEMPT; a later write from this checkout may already carry it to
# origin, so LOOK by CONTENT before you retry (round 4, 5203455553:
# SHA ancestry alone misses a peer's rebase of it, which changes the
# hash but not the patch); `git_timeout_die`'s network timeout on a
# push OR a pull, where a hung one often already landed; or six
# attempts exhausted because a peer's own uncommitted file blocks
# every rebase.
# 4 the mutex could not be taken within 60s
# 5 an edit moved more than one line and was refused — or, Amendment 15, a
# lane's object log could not be renamed to the row's own spelling
Expand Down Expand Up @@ -1162,14 +1172,47 @@ EOF
grep -n -A2 -e '^<<<<<<<' -- "$LANES_REPO/$cp" 2>/dev/null | head -n 40 >&2 || :
done
git -C "$LANES_REPO" rebase --abort 2>/dev/null || :
cp_sha="$(git -C "$LANES_REPO" rev-parse HEAD 2>/dev/null)"
note "rebase ABORTED — the worktree is clean and NOT mid-rebase. Your edit is safe in these local commits:"
git -C "$LANES_REPO" --no-pager log --oneline "origin/$LANES_BRANCH..HEAD" 2>/dev/null | head -n 10 >&2 || :
# #32 — AN ABORTED PULL IS NOT PROOF THIS COMMIT NEVER REACHED ORIGIN
# (the same rule RV-B3 took for the handoff stamp's own push, in
# lane-start). This checkout is shared with every lane on the
# workstation, so the very next write out of it pulls this dangling
# commit onto its own and pushes both together — three of the four
# "rebase conflict … nothing was pushed" lines seen on Eagle in one
# hour named a commit that was ALREADY on origin by the time anyone
# read them. LOOK before you reset or redo anything — by ANCESTRY
# (Copilot round 2 on #50, 5203033893): a log capped at a few entries
# can bury this commit below the window while it stays an ancestor, the
# moment four or more later writes land ahead of it.
# ROUND 3 (Copilot 5203261904): `fetch && merge-base ... && echo A ||
# echo B` is left-associative — a FAILED fetch also falls to the `||`
# and prints the SAME "not-there" a genuine non-ancestor does, so a
# stale or unreachable origin would silently wave the reset/redo path
# through. Fetch is now its own line, checked before anything else.
# ROUND 4 (Copilot 5203455553): SHA ancestry is the wrong test — the
# very write this whole recovery exists to catch (a peer's own
# `commit_push` pulling THIS dangling commit and rebasing it onto a
# newer base, per the very next branch above) gives it a NEW sha, so
# `merge-base --is-ancestor $cp_sha …` answers "not-there" even once
# the CONTENT is published. `git cherry` compares by PATCH, not by
# sha, so a rebase that only replays the same change (no real
# conflict) is still found. Measured against a real bare repo: a
# commit rebased by a peer this way answers `not-there` under
# `merge-base --is-ancestor` and `-` (found, equivalent) under
# `git cherry` — verified for the genuinely-absent case too (`+`) and
# the plain same-sha case (no output at all, which the `-c '^+'`
# count below also reads as zero, i.e. already there).
note "RECOVERY (in that order):"
note " git -C $LANES_REPO diff origin/$LANES_BRANCH..HEAD -- ${CP_PATHS[*]} # read back exactly what you wrote"
note " git -C $LANES_REPO reset --hard origin/$LANES_BRANCH # drop the local commits (NOTE: also drops any"
note " git -C $LANES_REPO fetch origin $LANES_BRANCH || echo FETCH-FAILED # if that printed FETCH-FAILED, STOP and retry — a stale or unreachable origin proves nothing either way"
note " git -C $LANES_REPO cherry origin/$LANES_BRANCH $cp_sha | grep -c '^+' # BY CONTENT, not by sha: a peer's rebase changes the hash but git cherry still finds the same patch"
note " if that printed 0, STOP — reset or redo now would write it a second time."
note " git -C $LANES_REPO diff origin/$LANES_BRANCH..HEAD -- ${CP_PATHS[*]} # only if it is not there: read back exactly what you wrote"
note " git -C $LANES_REPO reset --hard origin/$LANES_BRANCH # then drop the local commits (NOTE: also drops any"
note " # uncommitted peer edit in this checkout)"
note " then re-read LANES.md and redo the edit with lanes-edit.sh, on top of the peer's version."
die "rebase conflict on origin/$LANES_BRANCH — nothing was pushed." 3
die "rebase conflict on origin/$LANES_BRANCH — not pushed by this attempt; a later write from this checkout may already carry it, so look before you retry." 3
Comment thread
brettheap marked this conversation as resolved.
fi
attempt=$((attempt + 1))
sleep 3
Expand Down
148 changes: 148 additions & 0 deletions tests/test_lane_helpers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3497,6 +3497,154 @@ else
fi
rm -rf "$H_LOCK"

# =========================================================================
# #32 — `commit_push` NAMES THE ATTEMPT THAT ACTUALLY LANDED, NEVER AN
# EARLIER ONE ORIGIN HAS SINCE OVERTAKEN.
#
# Observed on Eagle, three to four times in one hour on 2026-09-13/14 while
# several lanes wrote the register concurrently: `append-row-status` /
# `append-line` printed "rebase conflict on origin/main — nothing was
# pushed" for a commit that WAS on origin by the time anyone read the line —
# carried out by the very next lanes-edit.sh write through this same shared
# checkout, which pulls this dangling local commit onto its own and pushes
# both together. A caller who trusted "nothing was pushed" and redid the
# write wrote a duplicate line (issue #32). The second clone plays the peer
# that moves origin between this invocation's own commit and its push,
# exactly as it does for the lost-claim race above.
#
# THIS SANDBOX'S OWN CHECKOUT IS ALREADY DIRTY, from the rejectgit cases
# above (a stamp whose push or pull was faked to fail leaves its commit
# local, and the fixture files those cases wrote are still uncommitted
# where their own act never got to commit them) — real content, and none of
# it this section's. `dirty_elsewhere` would read it as "someone else's
# uncommitted file" and route every case below through Amendment 5(d)'s
# skip-the-pull branch instead of the rebase this section means to exercise,
# so it is committed first, exactly as its own author would.
# -------------------------------------------------------------------------
if [ -n "$(git -C "$WIP" diff --name-only 2>/dev/null)" ]; then
git -C "$WIP" add -u -- .
git -C "$WIP" commit -q -m "fixture cleanup: commit content earlier cases left uncommitted, before the #32 race/conflict cases"
git -C "$WIP" pull -q --rebase origin main 2>/dev/null || :
git -C "$WIP" push -q origin main
fi
git -C "$CLONE2" pull -q --rebase origin main 2>/dev/null
RC_ID="aaaa0005-c0c0-4000-8000-aaaa0005c0c0"
"$E" add-row "| \`repoRC-1\` | harness \`$RC_ID\` | Eagle / test / brett | 2026-09-11T00:00Z | none | handoffs/repoRC/x.md | ACTIVE |" >/dev/null 2>&1

# -- a race a LATER attempt wins: not a conflict, and the message must name
# the attempt that actually pushed, never attempt 1's rejection — and the
# retry must not write the row's own text twice.
RACE_FLAG="$SANDBOX/race-peer-landed"
rm -f -- "$RACE_FLAG"
cat > "$SANDBOX/racegit" <<FAKE
#!/usr/bin/env bash
# Plays the second workstation, landing its own commit in the exact window
# the retry loop exists for: after this attempt's own pull --rebase has
# already succeeded and just before its push, so the push is rejected and
# the NEXT attempt's own pull --rebase is what actually meets the peer.
for a in "\$@"; do
if [ "\$a" = push ] && [ ! -e "$RACE_FLAG" ]; then
touch "$RACE_FLAG"
git -C "$CLONE2" pull -q --rebase origin main >/dev/null 2>&1
printf 'CLAIMED — lane repoH-2, session %s@Raven, %s, opensoft/repoH#92\n' \
"$DEAD_ID" "\$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$CLONE2/lanes/log/repoH-2.md"
git -C "$CLONE2" add -- lanes/log/repoH-2.md
git -C "$CLONE2" commit -q -m "LOG(repoH-2@Raven): lands between this attempt's pull and its push"
git -C "$CLONE2" push -q origin main >/dev/null 2>&1
fi
done
exec git "\$@"
FAKE
chmod +x "$SANDBOX/racegit"

run env LANES_GIT="$SANDBOX/racegit" "$E" append-row-status repoRC-1 "RACE-RETRY-WINS"
is "a peer landing between this attempt's pull and its push still exits 0" "$rc" 0
has "…the message names attempt 2, the one that actually pushed" "$err" "pushed origin/main (attempt 2)"
hasnt "…never claiming attempt 1 pushed it" "$err" "pushed origin/main (attempt 1)"
is "…and the retry writes the row's text once, never twice" "$(grep -c 'RACE-RETRY-WINS' "$LANES")" 1

# -- a race the rebase truly cannot resolve: the peer rewrote the SAME row.
# `rebase --abort` must leave the tree clean, the message must say so
# truthfully, and — because a write that runs after this one carries the
# dangling commit out regardless of what conflicted here — it must say to
# LOOK before resetting or redoing anything, the same rule RV-B3 took for
# the handoff stamp's own push in `lane-start`.
git -C "$CLONE2" pull -q --rebase origin main 2>/dev/null
PC_LINE="$(grep -n '^| `repoRC-1`' "$CLONE2/lanes/LANES.md" | head -n1 | cut -d: -f1)"
PC_ROW="$(sed -n -e "${PC_LINE}p" "$CLONE2/lanes/LANES.md")"
PC_TMP="$(mktemp -d)/pre"
cat -- "$CLONE2/lanes/LANES.md" > "$PC_TMP"
{ [ "$PC_LINE" -gt 1 ] && head -n "$((PC_LINE - 1))" -- "$PC_TMP"
printf '%s\n' "${PC_ROW%|}· PEER-CONFLICT |"
tail -n "+$((PC_LINE + 1))" -- "$PC_TMP"
} > "$CLONE2/lanes/LANES.md"
git -C "$CLONE2" add -- lanes/LANES.md
git -C "$CLONE2" commit -q -m "LANES(repoRC-1@Raven): peer rewrites the same row"
git -C "$CLONE2" push -q origin main

run "$E" append-row-status repoRC-1 "OURS-CONFLICT"
is "a peer's rewrite of the SAME row is a real conflict: exit 3" "$rc" 3
has "…REBASE CONFLICT is still named" "$err" "REBASE CONFLICT on origin/main"
has "…the abort runs and says the tree is clean" "$err" "rebase ABORTED — the worktree is clean and NOT mid-rebase"
is "…and it really is clean" "$(git -C "$WIP" status --porcelain | grep -c .)" 0
has "…told to fetch before resetting or redoing anything" "$err" \
"fetch origin main || echo FETCH-FAILED"
has "…and STOP named for a FAILED fetch too, never treated as leave to redo" "$err" \
"if that printed FETCH-FAILED, STOP and retry — a stale or unreachable origin proves nothing either way"

# Copilot's round-4 review of #50 (5203455553): the very peer write this
# recovery exists for carries a dangling commit by REBASING it onto a newer
# base — which the branch just above this one takes — and a rebase changes
# the commit's sha without changing its patch. `merge-base --is-ancestor
# $cp_sha …` (round 2's own fix) checks the OLD sha, so it can still answer
# "not-there" once the change is published under a new one. `git cherry`
# compares by PATCH, so it finds it either way; verified against a real bare
# repo for a peer-rebased commit (0, correctly ALREADY THERE — where
# `merge-base --is-ancestor` on the same sha answered "not-there"), a
# genuinely local-only one (1, correctly not-there), and a fetch against a
# bad remote (FETCH-FAILED, cherry never run against a stale ref).
has "…checked by CONTENT (git cherry), never by a sha a peer's rebase can change" "$err" \
"cherry origin/main"
cp_sha_printed="$(printf '%s\n' "$err" | grep -oE 'cherry origin/main [0-9a-f]{7,40}' | head -n1 | awk '{print $3}')"
is "…naming a real local commit to check, not a placeholder" \
"$([ -n "$cp_sha_printed" ] && printf 'has-sha' || printf 'MISSING')" "has-sha"
has "…counting only the patches git cherry could not find upstream" "$err" \
"grep -c '^+'"
has "…the reset is now conditional on that look, not unconditional" "$err" \
"only if it is not there: read back exactly what you wrote"
has "…and the die itself is scoped to this attempt, never a permanent claim" "$err" \
"not pushed by this attempt; a later write from this checkout may already carry it"
hasnt "…never the old unconditional claim an aborted pull cannot prove" "$err" \
"rebase conflict on origin/main — nothing was pushed."
pc_fetch="$(printf '%s\n' "$err" | grep -n -- 'fetch origin main || echo FETCH-FAILED' | head -n1 | cut -d: -f1)"
pc_look="$(printf '%s\n' "$err" | grep -n -- 'cherry origin/main' | head -n1 | cut -d: -f1)"
pc_reset="$(printf '%s\n' "$err" | grep -n -- 'reset --hard origin/main' | head -n1 | cut -d: -f1)"
is "…fetched, THEN checked by content, THEN reset — the same order RV-B3 pins" \
"$([ -n "$pc_fetch" ] && [ -n "$pc_look" ] && [ -n "$pc_reset" ] && [ "$pc_fetch" -lt "$pc_look" ] && [ "$pc_look" -lt "$pc_reset" ] && printf 'ordered' || printf "fetch $pc_fetch, look $pc_look, reset $pc_reset")" "ordered"

# Copilot's review of #50 (5202640056), suppressed comment on lanes-edit.sh:1180:
# the gate above was attached only to the diagnostic `diff` line — a caller
# whose fetch/look just showed the commit already on origin was still told, by
# the next two unconditional lines, to reset and redo, which is the exact
# duplicate this PR exists to prevent. A single STOP between the look and the
# reset gates all three lines below it (diff, reset, redo) at once, the same
# job the existing "only if it is not there" qualifier does for the diff line
# alone.
has "…and told to STOP there instead of resetting or redoing a landed commit" "$err" \
"if that printed 0, STOP — reset or redo now would write it a second time."
pc_stop="$(printf '%s\n' "$err" | grep -n -- 'STOP — reset or redo now' | head -n1 | cut -d: -f1)"
is "…the STOP sits between the look and the reset, gating both reset and redo" \
"$([ -n "$pc_look" ] && [ -n "$pc_stop" ] && [ -n "$pc_reset" ] && [ "$pc_look" -lt "$pc_stop" ] && [ "$pc_stop" -lt "$pc_reset" ] && printf 'ordered' || printf "look $pc_look, stop $pc_stop, reset $pc_reset")" "ordered"

# Suite housekeeping: this conflict is real and stays real (the peer's row
# rewrite is still on origin), so nothing later inherits this section's own
# dangling commit — and nothing later finds $WIP behind the peer's push
# either, which every case below this one assumes it never is. Fully synced
# to the CURRENT origin/main (the abort's own fetch already has it), not
# merely one commit back from wherever HEAD happened to be.
git -C "$WIP" fetch -q origin main 2>/dev/null || :
git -C "$WIP" reset --hard origin/main >/dev/null 2>&1

# --------------------------------------- the pointer file, when it does NOT answer
#
# AMENDMENT 9(a): "Failing to find it is a refusal, never a guess." Six ways it
Expand Down
Loading
Loading