From bab04b4b21f2accef9237af4ef63aa6cc8a81878 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 21:56:40 +0000 Subject: [PATCH 1/3] fix(scripts/pm): os-regen-merge marks the hand-off record and stops re-entering rerun MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 3's commit refusal exits before any record write, so the record stayed `pending` on the one path that finishes step 3 outside the script. `rr_classify` gated `rerun` on containment, which every later branch commit satisfies, so the next run redid step 2 against the pre-merge base and committed a revert of the operator's own regeneration — exit 0, nothing refused. Two closures: the refusal now marks the record `handoff` (step 2 discharged in the index, commit handed to the operator) and says re-running would redo step 2; and `rerun` now requires HEAD to BE the recorded merge, with a new `advanced` refusal for an unmarked record whose branch has moved past it. Claude-Session: https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi Co-authored-by: Claude --- scripts/pm/os-regen-merge.sh | 67 ++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/scripts/pm/os-regen-merge.sh b/scripts/pm/os-regen-merge.sh index c291f1d784d..01fae3b7328 100644 --- a/scripts/pm/os-regen-merge.sh +++ b/scripts/pm/os-regen-merge.sh @@ -573,7 +573,7 @@ rr_read_record() { # Repeated key, so this one is read as a SET rather than by `rr_field`. rr_rec_conflicted="$RR_NL$(awk 'index($0, "conflicted=") == 1 { print substr($0, 12) }' "$rr_record")$RR_NL" case "$rr_rec_phase" in - pending | done) ;; + pending | handoff | done) ;; *) return 0 ;; esac for rr_sha in "$rr_rec_base" "$rr_rec_branch_tip" "$rr_rec_main_tip"; do @@ -626,6 +626,21 @@ rr_write_record() { # history is the safe one: a missing ancestor object answers "not contained", # which routes to `plain` — today's behaviour — and never to a repair on a base # this script cannot prove. + +# Is HEAD the very merge this record was written for? ⚠️ Containment is NOT that +# question and using it for the rerun was the measured defect: every later commit +# on the branch contains the recorded pre-merge tip too, so a run made AFTER the +# operator finished step 3 by hand re-entered `rerun`, pinned the base back to the +# pre-merge shas, and took main's side over their own regeneration commit — which +# step 3 then COMMITTED, exit 0, nothing refused. Equality is the question, and +# both parents answer it: the recorded merge is the commit whose first parent is +# the recorded pre-merge tip and whose second is the main tip that was merged. +rr_head_is_recorded_merge() { + rr_h1="$(git rev-parse --verify --quiet 'HEAD^1')" || return 1 + rr_h2="$(git rev-parse --verify --quiet 'HEAD^2')" || return 1 + [ "$rr_h1" = "$rr_rec_branch_tip" ] && [ "$rr_h2" = "$rr_rec_main_tip" ] +} + rr_classify() { if [ "$rr_have_record" = yes ]; then if [ "$rr_record_ok" != yes ]; then @@ -638,8 +653,17 @@ rr_classify() { fi if git merge-base --is-ancestor "$rr_rec_main_tip" HEAD 2>/dev/null; then # The recorded merge is IN this HEAD, so step 1 is behind us either way. - if [ "$rr_rec_phase" != done ]; then + # ⛔ Redoing step 2 is sound ONLY while HEAD still IS that merge: a branch + # commit past it is work step 2 would discard. `handoff` means step 3's + # commit was refused and step 2 was left discharged in the index, so HEAD + # back AT the merge means the operator dropped that index and step 2 is + # owed again; HEAD past it means they committed and step 4 is what is owed. + if [ "$rr_rec_phase" != done ] && rr_head_is_recorded_merge; then echo rerun + elif [ "$rr_rec_phase" = pending ]; then + # Never marked, yet the branch moved past the merge: who discharged step 2 + # is unreadable from here, so it is refused rather than redone. + echo advanced elif [ "$(git rev-parse origin/main)" = "$rr_rec_main_tip" ]; then echo settled else @@ -673,6 +697,31 @@ rr_refuse_stale() { exit 1 } +# The record still says `pending` and the branch has commits PAST the recorded +# merge — the state step 3's refusal used to leave behind, because it exits before +# marking anything and its own instruction finishes the commit OUTSIDE this script. +# Marked `handoff` that refusal no longer lands here; an unmarked record (a run +# killed after step 1, or one written before this refusal existed) still does. +rr_refuse_advanced() { + echo "✗ the recorded merge is in HEAD, the record still says \`pending\`, and this branch has" >&2 + echo " commits PAST that merge — refusing to redo step 2 over them." >&2 + rr_print_record + echo " \`pending\` means no run ever marked step 2 discharged; the commits past the merge mean" >&2 + echo " somebody did something this script cannot read. Redoing step 2 now would pin the base to" >&2 + echo " the PRE-MERGE shas above and take main's side of every generated path both sides moved —" >&2 + echo " discarding those later commits' bytes — and step 3 would COMMIT that, exit 0." >&2 + echo " ⛔ That is a revert wearing a repair's message." >&2 + echo " If you finished step 3 BY HAND (its refusal says to), step 2 is already discharged and" >&2 + echo " the sequence resumes at STEP 4 — drop the spent record and go there:" >&2 + echo " rm -f $rr_record" >&2 + echo " If it is NOT discharged, perform step 2 by hand against the RECORDED base first:" >&2 + echo " git diff --name-only $rr_rec_base $rr_rec_main_tip -- " >&2 + echo " git diff --name-only $rr_rec_base $rr_rec_branch_tip -- " >&2 + echo " git restore --source=$rr_rec_main_tip -- # tree only, ⛔ never --staged" >&2 + echo " then commit that as its own commit and continue with step 4." >&2 + exit 1 +} + # The state the old conflict messages sent operators into, now named out loud. rr_refuse_orphan() { echo "✗ HEAD already contains origin/main and NO pre-merge base is recorded — refusing to run" >&2 @@ -759,6 +808,7 @@ mode_run() { rr_mode="$(rr_classify)" case "$rr_mode" in stale) rr_refuse_stale ;; + advanced) rr_refuse_advanced ;; orphan) rr_refuse_orphan ;; settled) rr_report_settled @@ -1132,10 +1182,23 @@ mode_run() { if [ -n "$(git status --porcelain)" ]; then git add -A if ! git commit --no-edit -m "merge origin/main (os-regen artifacts taken from main; regeneration follows)"; then + # ⛔ MARKED before this exit, and that marking is the repair: step 2 is + # discharged in the index at this instant, and the line below hands the + # commit to the operator — the ONE path that finishes step 3 outside this + # script. Left `pending`, the next run read containment, re-entered `rerun` + # and took main's side back over whatever that hand-off committed. The + # recorded conflict set rides along so a rerun after a discarded index + # still keeps the resolutions it asked for. + rr_write_record handoff "$merge_base" "$branch_tip" "$main_side" "$rr_rec_conflicted" echo "✗ step 3's commit was refused — the merge is staged but NOT committed." >&2 echo " ⛔ Do not regenerate yet: with a staged index a bare \`git commit\` after" >&2 echo " regeneration lands the STAGED side, not the tree you inspected." >&2 echo " Clear what the hook reported, then \`git add -A && git commit\` before step 4." >&2 + echo " Step 2 IS discharged in that index and the record now says so (phase=handoff):" >&2 + echo " $rr_record" >&2 + echo " ⛔ Do NOT rerun this script to finish that commit — it would redo step 2 against" >&2 + echo " the pre-merge base and take main's side back over what you just committed." >&2 + echo " Once the commit exists the sequence resumes at STEP 4." >&2 exit 1 fi else From a999f522a9d599ba85b2d8e2dd532eb4e6552444 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 22:03:58 +0000 Subject: [PATCH 2/3] test(scripts/pm): pin repro 1 and its discriminating mutation in the self-test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Case 11 replays the card's sequence on the rerun fixture — conflict, hand resolution, a refused step-3 commit, the by-hand hand-off commit — and pins that the record reads `handoff`, that the next run reports the sequence discharged without re-entering `rerun`, and that the regeneration (`FLOWX`) survives. It also pins the new refusal for a record nothing ever marked. Case 11b puts the containment gate back by literal replacement and replays run 3 in a copy of that tree: the run re-enters `rerun`, takes main's side back and step 3 commits the revert with exit 0 — the card, reproduced on demand. Claude-Session: https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi Co-authored-by: Claude --- scripts/pm/os-regen-merge.sh | 117 +++++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 45 deletions(-) diff --git a/scripts/pm/os-regen-merge.sh b/scripts/pm/os-regen-merge.sh index 01fae3b7328..aecf262e3e7 100644 --- a/scripts/pm/os-regen-merge.sh +++ b/scripts/pm/os-regen-merge.sh @@ -628,17 +628,13 @@ rr_write_record() { # this script cannot prove. # Is HEAD the very merge this record was written for? ⚠️ Containment is NOT that -# question and using it for the rerun was the measured defect: every later commit -# on the branch contains the recorded pre-merge tip too, so a run made AFTER the -# operator finished step 3 by hand re-entered `rerun`, pinned the base back to the -# pre-merge shas, and took main's side over their own regeneration commit — which -# step 3 then COMMITTED, exit 0, nothing refused. Equality is the question, and -# both parents answer it: the recorded merge is the commit whose first parent is -# the recorded pre-merge tip and whose second is the main tip that was merged. +# question, and asking it for the rerun was the measured defect: every later branch +# commit contains the recorded pre-merge tip too, so a run made after the operator +# finished step 3 BY HAND re-entered `rerun`, took main's side back over their own +# regeneration commit, and step 3 COMMITTED it. Both parents answer equality. rr_head_is_recorded_merge() { - rr_h1="$(git rev-parse --verify --quiet 'HEAD^1')" || return 1 - rr_h2="$(git rev-parse --verify --quiet 'HEAD^2')" || return 1 - [ "$rr_h1" = "$rr_rec_branch_tip" ] && [ "$rr_h2" = "$rr_rec_main_tip" ] + [ "$(git rev-parse --verify --quiet 'HEAD^1' || true)" = "$rr_rec_branch_tip" ] && + [ "$(git rev-parse --verify --quiet 'HEAD^2' || true)" = "$rr_rec_main_tip" ] } rr_classify() { @@ -653,16 +649,14 @@ rr_classify() { fi if git merge-base --is-ancestor "$rr_rec_main_tip" HEAD 2>/dev/null; then # The recorded merge is IN this HEAD, so step 1 is behind us either way. - # ⛔ Redoing step 2 is sound ONLY while HEAD still IS that merge: a branch - # commit past it is work step 2 would discard. `handoff` means step 3's - # commit was refused and step 2 was left discharged in the index, so HEAD - # back AT the merge means the operator dropped that index and step 2 is - # owed again; HEAD past it means they committed and step 4 is what is owed. + # ⛔ Redoing step 2 is sound ONLY while HEAD still IS that merge — a commit past + # it is work step 2 would discard. `handoff` (step 3 refused, the index left + # discharged) AT the merge means that index was dropped and step 2 is owed again; + # PAST it means the operator committed, so step 4 is owed. A `pending` record past + # its merge says nobody ever marked step 2 done — refused below, ⛔ never redone. if [ "$rr_rec_phase" != done ] && rr_head_is_recorded_merge; then echo rerun elif [ "$rr_rec_phase" = pending ]; then - # Never marked, yet the branch moved past the merge: who discharged step 2 - # is unreadable from here, so it is refused rather than redone. echo advanced elif [ "$(git rev-parse origin/main)" = "$rr_rec_main_tip" ]; then echo settled @@ -697,28 +691,23 @@ rr_refuse_stale() { exit 1 } -# The record still says `pending` and the branch has commits PAST the recorded -# merge — the state step 3's refusal used to leave behind, because it exits before -# marking anything and its own instruction finishes the commit OUTSIDE this script. -# Marked `handoff` that refusal no longer lands here; an unmarked record (a run -# killed after step 1, or one written before this refusal existed) still does. +# Recorded `pending`, yet the branch has commits PAST the recorded merge — the state +# step 3's refusal used to leave behind, since it exits before marking anything while +# instructing the operator to finish the commit OUTSIDE this script. Marked `handoff` +# that refusal no longer lands here; a record nothing ever marked still does. rr_refuse_advanced() { echo "✗ the recorded merge is in HEAD, the record still says \`pending\`, and this branch has" >&2 echo " commits PAST that merge — refusing to redo step 2 over them." >&2 rr_print_record - echo " \`pending\` means no run ever marked step 2 discharged; the commits past the merge mean" >&2 - echo " somebody did something this script cannot read. Redoing step 2 now would pin the base to" >&2 - echo " the PRE-MERGE shas above and take main's side of every generated path both sides moved —" >&2 - echo " discarding those later commits' bytes — and step 3 would COMMIT that, exit 0." >&2 - echo " ⛔ That is a revert wearing a repair's message." >&2 - echo " If you finished step 3 BY HAND (its refusal says to), step 2 is already discharged and" >&2 - echo " the sequence resumes at STEP 4 — drop the spent record and go there:" >&2 - echo " rm -f $rr_record" >&2 - echo " If it is NOT discharged, perform step 2 by hand against the RECORDED base first:" >&2 + echo " \`pending\` means no run ever marked step 2 discharged, and the later commits mean" >&2 + echo " somebody did. Redoing it now pins the base to the PRE-MERGE shas above, takes main's" >&2 + echo " side of every generated path both sides moved — discarding those commits' bytes — and" >&2 + echo " step 3 COMMITS that, exit 0: ⛔ a revert wearing a repair's message. Finished step 3 BY" >&2 + echo " HAND, as its refusal says to? Then step 2 IS discharged: \`rm -f $rr_record\` and resume" >&2 + echo " at STEP 4. If it is NOT, do step 2 by hand against the RECORDED base, commit, then go:" >&2 echo " git diff --name-only $rr_rec_base $rr_rec_main_tip -- " >&2 echo " git diff --name-only $rr_rec_base $rr_rec_branch_tip -- " >&2 - echo " git restore --source=$rr_rec_main_tip -- # tree only, ⛔ never --staged" >&2 - echo " then commit that as its own commit and continue with step 4." >&2 + echo " git restore --source=$rr_rec_main_tip -- # tree only" >&2 exit 1 } @@ -1182,23 +1171,20 @@ mode_run() { if [ -n "$(git status --porcelain)" ]; then git add -A if ! git commit --no-edit -m "merge origin/main (os-regen artifacts taken from main; regeneration follows)"; then - # ⛔ MARKED before this exit, and that marking is the repair: step 2 is - # discharged in the index at this instant, and the line below hands the - # commit to the operator — the ONE path that finishes step 3 outside this - # script. Left `pending`, the next run read containment, re-entered `rerun` - # and took main's side back over whatever that hand-off committed. The - # recorded conflict set rides along so a rerun after a discarded index - # still keeps the resolutions it asked for. + # ⛔ MARKED before this exit, and the marking is the repair: step 2 is discharged in + # the index at this instant and the line below hands the commit to the operator — the + # ONE path that finishes step 3 outside this script. Left `pending`, the next run read + # containment, re-entered `rerun` and took main's side back over whatever that hand-off + # committed. The conflict set rides along, so a DROPPED index still keeps resolutions. rr_write_record handoff "$merge_base" "$branch_tip" "$main_side" "$rr_rec_conflicted" echo "✗ step 3's commit was refused — the merge is staged but NOT committed." >&2 echo " ⛔ Do not regenerate yet: with a staged index a bare \`git commit\` after" >&2 echo " regeneration lands the STAGED side, not the tree you inspected." >&2 echo " Clear what the hook reported, then \`git add -A && git commit\` before step 4." >&2 - echo " Step 2 IS discharged in that index and the record now says so (phase=handoff):" >&2 - echo " $rr_record" >&2 - echo " ⛔ Do NOT rerun this script to finish that commit — it would redo step 2 against" >&2 - echo " the pre-merge base and take main's side back over what you just committed." >&2 - echo " Once the commit exists the sequence resumes at STEP 4." >&2 + echo " Step 2 IS discharged in that index and the record says so (phase=handoff), so the" >&2 + echo " sequence resumes at STEP 4 once that commit exists: $rr_record" >&2 + echo " ⛔ Do NOT rerun this script to finish it — that redoes step 2 against the pre-merge" >&2 + echo " base and takes main's side back over what you just committed." >&2 exit 1 fi else @@ -2161,6 +2147,47 @@ mode_self_test() { "$(git show HEAD:gen/deferred.txt | grep -c 'deferred v1-MAIN' || true)" 0 cd "$here" + # --- 11. REPRO 1 (this card): step 3's commit REFUSED hands the commit to the + # operator, and until the record said so the NEXT run re-entered `rerun`, redid + # step 2 against the pre-merge base and COMMITTED a revert of the operator's own + # regeneration — exit 0, nothing refused. `FLOWX` stands in for it. + st_fixture_rerun "$tmp/m" + bash "$SELF" >/dev/null 2>&1 || true # run 1 stops on the MIXED conflict + st_resolve_and_commit && st_record="$(rr_record_path)" # the merge, committed by hand + printf '#!/bin/sh\nexit 1\n' > .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit + out="$(bash "$SELF" 2>&1)" && rc=0 || rc=$? # run 2: the rerun, step 3 refused + st_case 'r1: the refused step-3 commit fails, MARKS handoff, warns against rerunning' \ + "$rc/$(rr_field phase "$st_record")/$(printf '%s' "$out" | grep -c 'Do NOT rerun this script' || true)" '1/handoff/1' + # Then do EXACTLY what it says: clear the hook, regenerate, `git add -A && commit`. + rm -f .git/hooks/pre-commit && printf 'deferred v1-MAIN\nFLOWX\n' > gen/deferred.txt + git add -A && git commit -qm 'step 4 by hand: regenerate (FLOWX)' + cp -a "$tmp/m" "$tmp/m-mut" # 11b replays run 3 from right here + out="$(bash "$SELF" 2>&1)" && rc=0 || rc=$? # run 3: the card's own run + st_case 'r1: run 3 exits 0 discharged — no rerun, no side taken, and FLOWX SURVIVES (the card read 0)' \ + "$rc/$(printf '%s' "$out" | grep -c '^→ RERUN:' || true)/$(printf '%s' "$out" | grep -c 'already discharged' || true)/$(printf '%s' "$out" | grep -c "TAKING main.s side" || true)/$(git show HEAD:gen/deferred.txt | grep -c FLOWX || true)" '0/0/1/0/1' + # The safety net for a record nothing ever marked (a run killed after step 1, or one + # written before the marking existed): refused, ⛔ never silently redone. + sed 's/^phase=.*/phase=pending/' "$st_record" > "$st_record.t" && mv "$st_record.t" "$st_record" + out="$(bash "$SELF" 2>&1)" && rc=0 || rc=$? + st_case 'r1: a pending record past its own merge is REFUSED with the by-hand step 2' \ + "$rc/$(printf '%s' "$out" | grep -c "git restore --source=$(rr_field main_tip "$st_record")" || true)" '1/1' + cd "$here" + + # --- 11b. THE DISCRIMINATING MUTATION for case 11: put the containment gate back — + # the ONE line — and the card reproduces on demand, in the tree case 11 copied + # aside one commit earlier. Same perl/\Q..\E replacement 6b, 8b, 9b, 10b use. + mutated_reentry="$tmp/mutated-reentry-os-regen-merge.sh" + MUT_ANCHOR=' if [ "$rr_rec_phase" != done ] && rr_head_is_recorded_merge; then' \ + MUT_INSERT=' if [ "$rr_rec_phase" != done ]; then' \ + perl -0777 -pe 's/\Q$ENV{MUT_ANCHOR}\E/$ENV{MUT_INSERT}/' "$SELF" > "$mutated_reentry" + st_case 'the equality gate has ONE call site, the mutation took, and it parses' \ + "$(sed -n '1,/^# --- self-test/p' "$SELF" | grep -c 'rr_head_is_recorded_merge; then' || true)/$(diff -q "$SELF" "$mutated_reentry" >/dev/null 2>&1; echo $?)/$(bash -n "$mutated_reentry" >/dev/null 2>&1; echo $?)" '1/1/0' + cd "$tmp/m-mut/work" + mut_out="$(bash "$mutated_reentry" 2>&1)" && mut_rc=0 || mut_rc=$? + st_case 'mutated: run 3 re-enters the rerun, exits 0, takes main s side back, and step 3 COMMITS the revert — FLOWX gone' \ + "$mut_rc/$(printf '%s' "$mut_out" | grep -c '^→ RERUN:' || true)/$(printf '%s' "$mut_out" | grep -c 'TAKING main.s side of gen/deferred.txt' || true)/$(git show HEAD:gen/deferred.txt | grep -c FLOWX || true)" '0/1/1/0' + cd "$here" + if [ "$st_fail" -ne 0 ]; then printf '✗ os-regen-merge self-test: %d case(s) failed.\n' "$st_fail" return 1 From aa5a2b71e32a6d200fe9c44dd16d0a2ffd148580 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 22:17:31 +0000 Subject: [PATCH 3/3] fix(scripts/pm): mark the record at the hand-off assertion's exit too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The uncommitted-regen-path refusal is the second exit that leaves step 2 discharged — committed, in that one's case — and it also left the record `pending`. With the rerun now gated on equality that would refuse the next run instead of redoing step 2, which is safe but points the operator at the wrong remedy. Marked `handoff`, the next clean-tree run reports the sequence discharged and resumes at step 4. Claude-Session: https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi Co-authored-by: Claude --- scripts/pm/os-regen-merge.sh | 42 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/scripts/pm/os-regen-merge.sh b/scripts/pm/os-regen-merge.sh index aecf262e3e7..8670b8ed83e 100644 --- a/scripts/pm/os-regen-merge.sh +++ b/scripts/pm/os-regen-merge.sh @@ -627,11 +627,10 @@ rr_write_record() { # which routes to `plain` — today's behaviour — and never to a repair on a base # this script cannot prove. -# Is HEAD the very merge this record was written for? ⚠️ Containment is NOT that -# question, and asking it for the rerun was the measured defect: every later branch -# commit contains the recorded pre-merge tip too, so a run made after the operator -# finished step 3 BY HAND re-entered `rerun`, took main's side back over their own -# regeneration commit, and step 3 COMMITTED it. Both parents answer equality. +# Is HEAD the very merge this record was written for? ⚠️ Containment is NOT that question, and +# asking it for the rerun was the measured defect: every later branch commit contains the recorded +# pre-merge tip too, so a run made after the operator finished step 3 BY HAND re-entered `rerun`, +# took main's side back over their own regeneration commit, and step 3 COMMITTED it. rr_head_is_recorded_merge() { [ "$(git rev-parse --verify --quiet 'HEAD^1' || true)" = "$rr_rec_branch_tip" ] && [ "$(git rev-parse --verify --quiet 'HEAD^2' || true)" = "$rr_rec_main_tip" ] @@ -700,11 +699,11 @@ rr_refuse_advanced() { echo " commits PAST that merge — refusing to redo step 2 over them." >&2 rr_print_record echo " \`pending\` means no run ever marked step 2 discharged, and the later commits mean" >&2 - echo " somebody did. Redoing it now pins the base to the PRE-MERGE shas above, takes main's" >&2 - echo " side of every generated path both sides moved — discarding those commits' bytes — and" >&2 - echo " step 3 COMMITS that, exit 0: ⛔ a revert wearing a repair's message. Finished step 3 BY" >&2 - echo " HAND, as its refusal says to? Then step 2 IS discharged: \`rm -f $rr_record\` and resume" >&2 - echo " at STEP 4. If it is NOT, do step 2 by hand against the RECORDED base, commit, then go:" >&2 + echo " somebody did. Redoing it now pins the base to the PRE-MERGE shas above, takes main's side" >&2 + echo " of every generated path both sides moved — discarding those commits' bytes — and step 3" >&2 + echo " COMMITS that, exit 0: ⛔ a revert wearing a repair's message. Finished step 3 BY HAND, as" >&2 + echo " its refusal says to? Then step 2 IS discharged: \`rm -f $rr_record\` and resume at STEP 4." >&2 + echo " If NOT, do step 2 by hand against the RECORDED base, commit, and continue with step 4:" >&2 echo " git diff --name-only $rr_rec_base $rr_rec_main_tip -- " >&2 echo " git diff --name-only $rr_rec_base $rr_rec_branch_tip -- " >&2 echo " git restore --source=$rr_rec_main_tip -- # tree only" >&2 @@ -1192,9 +1191,12 @@ mode_run() { fi # Hand-off assertion: step 4 regenerates on top of this tree and commits the - # result, so anything uncommitted here rides into that commit unread. + # result, so anything uncommitted here rides into that commit unread. ⛔ This exit marks + # the record too: step 2 is COMMITTED by now, and a record left `pending` would send the + # next run back through step 2 over it. handoff="$(git status --porcelain -- "${regen_paths[@]}")" if [ -n "$handoff" ]; then + rr_write_record handoff "$merge_base" "$branch_tip" "$main_side" "$rr_rec_conflicted" echo "✗ refusing to hand off to step 4 — generated paths are not committed:" >&2 printf '%s\n' "$handoff" >&2 echo " A regen path whose INDEX and WORKTREE disagree (porcelain \`MM\`) is the trap:" >&2 @@ -2147,10 +2149,10 @@ mode_self_test() { "$(git show HEAD:gen/deferred.txt | grep -c 'deferred v1-MAIN' || true)" 0 cd "$here" - # --- 11. REPRO 1 (this card): step 3's commit REFUSED hands the commit to the - # operator, and until the record said so the NEXT run re-entered `rerun`, redid - # step 2 against the pre-merge base and COMMITTED a revert of the operator's own - # regeneration — exit 0, nothing refused. `FLOWX` stands in for it. + # --- 11. REPRO 1 (this card): step 3's commit REFUSED hands the commit to the operator, and + # until the record said so the NEXT run re-entered `rerun`, redid step 2 against the + # pre-merge base and COMMITTED a revert of that operator's own regeneration (`FLOWX` + # below) — exit 0, nothing refused. st_fixture_rerun "$tmp/m" bash "$SELF" >/dev/null 2>&1 || true # run 1 stops on the MIXED conflict st_resolve_and_commit && st_record="$(rr_record_path)" # the merge, committed by hand @@ -2158,15 +2160,13 @@ mode_self_test() { out="$(bash "$SELF" 2>&1)" && rc=0 || rc=$? # run 2: the rerun, step 3 refused st_case 'r1: the refused step-3 commit fails, MARKS handoff, warns against rerunning' \ "$rc/$(rr_field phase "$st_record")/$(printf '%s' "$out" | grep -c 'Do NOT rerun this script' || true)" '1/handoff/1' - # Then do EXACTLY what it says: clear the hook, regenerate, `git add -A && commit`. - rm -f .git/hooks/pre-commit && printf 'deferred v1-MAIN\nFLOWX\n' > gen/deferred.txt + rm -f .git/hooks/pre-commit && printf 'deferred v1-MAIN\nFLOWX\n' > gen/deferred.txt # as told git add -A && git commit -qm 'step 4 by hand: regenerate (FLOWX)' cp -a "$tmp/m" "$tmp/m-mut" # 11b replays run 3 from right here out="$(bash "$SELF" 2>&1)" && rc=0 || rc=$? # run 3: the card's own run st_case 'r1: run 3 exits 0 discharged — no rerun, no side taken, and FLOWX SURVIVES (the card read 0)' \ "$rc/$(printf '%s' "$out" | grep -c '^→ RERUN:' || true)/$(printf '%s' "$out" | grep -c 'already discharged' || true)/$(printf '%s' "$out" | grep -c "TAKING main.s side" || true)/$(git show HEAD:gen/deferred.txt | grep -c FLOWX || true)" '0/0/1/0/1' - # The safety net for a record nothing ever marked (a run killed after step 1, or one - # written before the marking existed): refused, ⛔ never silently redone. + # The safety net for a record nothing ever marked: refused, ⛔ never silently redone. sed 's/^phase=.*/phase=pending/' "$st_record" > "$st_record.t" && mv "$st_record.t" "$st_record" out="$(bash "$SELF" 2>&1)" && rc=0 || rc=$? st_case 'r1: a pending record past its own merge is REFUSED with the by-hand step 2' \ @@ -2174,8 +2174,8 @@ mode_self_test() { cd "$here" # --- 11b. THE DISCRIMINATING MUTATION for case 11: put the containment gate back — - # the ONE line — and the card reproduces on demand, in the tree case 11 copied - # aside one commit earlier. Same perl/\Q..\E replacement 6b, 8b, 9b, 10b use. + # the ONE line — and the card reproduces on demand, in the tree case 11 copied aside + # one commit earlier. Same perl/\Q..\E replacement 6b, 8b, 9b and 10b use. mutated_reentry="$tmp/mutated-reentry-os-regen-merge.sh" MUT_ANCHOR=' if [ "$rr_rec_phase" != done ] && rr_head_is_recorded_merge; then' \ MUT_INSERT=' if [ "$rr_rec_phase" != done ]; then' \