Skip to content

Commit bab04b4

Browse files
committed
fix(scripts/pm): os-regen-merge marks the hand-off record and stops re-entering rerun
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 <noreply@anthropic.com>
1 parent 23f1de0 commit bab04b4

1 file changed

Lines changed: 65 additions & 2 deletions

File tree

‎scripts/pm/os-regen-merge.sh‎

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
629644
rr_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.
677726
rr_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

Comments
 (0)