@@ -573,7 +573,7 @@ rr_read_record() {
573573 # Repeated key, so this one is read as a SET rather than by `rr_field`.
574574 rr_rec_conflicted=" $RR_NL $( awk ' index($0, "conflicted=") == 1 { print substr($0, 12) }' " $rr_record " ) $RR_NL "
575575 case " $rr_rec_phase " in
576- pending | done) ;;
576+ pending | handoff | done) ;;
577577 * ) return 0 ;;
578578 esac
579579 for rr_sha in " $rr_rec_base " " $rr_rec_branch_tip " " $rr_rec_main_tip " ; do
@@ -626,6 +626,21 @@ rr_write_record() {
626626# history is the safe one: a missing ancestor object answers "not contained",
627627# which routes to `plain` — today's behaviour — and never to a repair on a base
628628# this script cannot prove.
629+
630+ # Is HEAD the very merge this record was written for? ⚠️ Containment is NOT that
631+ # question and using it for the rerun was the measured defect: every later commit
632+ # on the branch contains the recorded pre-merge tip too, so a run made AFTER the
633+ # operator finished step 3 by hand re-entered `rerun`, pinned the base back to the
634+ # pre-merge shas, and took main's side over their own regeneration commit — which
635+ # step 3 then COMMITTED, exit 0, nothing refused. Equality is the question, and
636+ # both parents answer it: the recorded merge is the commit whose first parent is
637+ # the recorded pre-merge tip and whose second is the main tip that was merged.
638+ rr_head_is_recorded_merge () {
639+ rr_h1=" $( git rev-parse --verify --quiet ' HEAD^1' ) " || return 1
640+ rr_h2=" $( git rev-parse --verify --quiet ' HEAD^2' ) " || return 1
641+ [ " $rr_h1 " = " $rr_rec_branch_tip " ] && [ " $rr_h2 " = " $rr_rec_main_tip " ]
642+ }
643+
629644rr_classify () {
630645 if [ " $rr_have_record " = yes ]; then
631646 if [ " $rr_record_ok " != yes ]; then
@@ -638,8 +653,17 @@ rr_classify() {
638653 fi
639654 if git merge-base --is-ancestor " $rr_rec_main_tip " HEAD 2> /dev/null; then
640655 # The recorded merge is IN this HEAD, so step 1 is behind us either way.
641- if [ " $rr_rec_phase " != done ]; then
656+ # ⛔ Redoing step 2 is sound ONLY while HEAD still IS that merge: a branch
657+ # commit past it is work step 2 would discard. `handoff` means step 3's
658+ # commit was refused and step 2 was left discharged in the index, so HEAD
659+ # back AT the merge means the operator dropped that index and step 2 is
660+ # owed again; HEAD past it means they committed and step 4 is what is owed.
661+ if [ " $rr_rec_phase " != done ] && rr_head_is_recorded_merge; then
642662 echo rerun
663+ elif [ " $rr_rec_phase " = pending ]; then
664+ # Never marked, yet the branch moved past the merge: who discharged step 2
665+ # is unreadable from here, so it is refused rather than redone.
666+ echo advanced
643667 elif [ " $( git rev-parse origin/main) " = " $rr_rec_main_tip " ]; then
644668 echo settled
645669 else
@@ -673,6 +697,31 @@ rr_refuse_stale() {
673697 exit 1
674698}
675699
700+ # The record still says `pending` and the branch has commits PAST the recorded
701+ # merge — the state step 3's refusal used to leave behind, because it exits before
702+ # marking anything and its own instruction finishes the commit OUTSIDE this script.
703+ # Marked `handoff` that refusal no longer lands here; an unmarked record (a run
704+ # killed after step 1, or one written before this refusal existed) still does.
705+ rr_refuse_advanced () {
706+ echo " ✗ the recorded merge is in HEAD, the record still says \` pending\` , and this branch has" >&2
707+ echo " commits PAST that merge — refusing to redo step 2 over them." >&2
708+ rr_print_record
709+ echo " \` pending\` means no run ever marked step 2 discharged; the commits past the merge mean" >&2
710+ echo " somebody did something this script cannot read. Redoing step 2 now would pin the base to" >&2
711+ echo " the PRE-MERGE shas above and take main's side of every generated path both sides moved —" >&2
712+ echo " discarding those later commits' bytes — and step 3 would COMMIT that, exit 0." >&2
713+ echo " ⛔ That is a revert wearing a repair's message." >&2
714+ echo " If you finished step 3 BY HAND (its refusal says to), step 2 is already discharged and" >&2
715+ echo " the sequence resumes at STEP 4 — drop the spent record and go there:" >&2
716+ echo " rm -f $rr_record " >&2
717+ echo " If it is NOT discharged, perform step 2 by hand against the RECORDED base first:" >&2
718+ echo " git diff --name-only $rr_rec_base $rr_rec_main_tip -- <the merge=os-regen patterns>" >&2
719+ echo " git diff --name-only $rr_rec_base $rr_rec_branch_tip -- <the same patterns>" >&2
720+ echo " git restore --source=$rr_rec_main_tip -- <every path in BOTH lists> # tree only, ⛔ never --staged" >&2
721+ echo " then commit that as its own commit and continue with step 4." >&2
722+ exit 1
723+ }
724+
676725# The state the old conflict messages sent operators into, now named out loud.
677726rr_refuse_orphan () {
678727 echo " ✗ HEAD already contains origin/main and NO pre-merge base is recorded — refusing to run" >&2
@@ -759,6 +808,7 @@ mode_run() {
759808 rr_mode=" $( rr_classify) "
760809 case " $rr_mode " in
761810 stale) rr_refuse_stale ;;
811+ advanced) rr_refuse_advanced ;;
762812 orphan) rr_refuse_orphan ;;
763813 settled)
764814 rr_report_settled
@@ -1132,10 +1182,23 @@ mode_run() {
11321182 if [ -n " $( git status --porcelain) " ]; then
11331183 git add -A
11341184 if ! git commit --no-edit -m " merge origin/main (os-regen artifacts taken from main; regeneration follows)" ; then
1185+ # ⛔ MARKED before this exit, and that marking is the repair: step 2 is
1186+ # discharged in the index at this instant, and the line below hands the
1187+ # commit to the operator — the ONE path that finishes step 3 outside this
1188+ # script. Left `pending`, the next run read containment, re-entered `rerun`
1189+ # and took main's side back over whatever that hand-off committed. The
1190+ # recorded conflict set rides along so a rerun after a discarded index
1191+ # still keeps the resolutions it asked for.
1192+ rr_write_record handoff " $merge_base " " $branch_tip " " $main_side " " $rr_rec_conflicted "
11351193 echo " ✗ step 3's commit was refused — the merge is staged but NOT committed." >&2
11361194 echo " ⛔ Do not regenerate yet: with a staged index a bare \` git commit\` after" >&2
11371195 echo " regeneration lands the STAGED side, not the tree you inspected." >&2
11381196 echo " Clear what the hook reported, then \` git add -A && git commit\` before step 4." >&2
1197+ echo " Step 2 IS discharged in that index and the record now says so (phase=handoff):" >&2
1198+ echo " $rr_record " >&2
1199+ echo " ⛔ Do NOT rerun this script to finish that commit — it would redo step 2 against" >&2
1200+ echo " the pre-merge base and take main's side back over what you just committed." >&2
1201+ echo " Once the commit exists the sequence resumes at STEP 4." >&2
11391202 exit 1
11401203 fi
11411204 else
0 commit comments