Skip to content

A failed Self-merge goes back round the Repair loop; policy refusals leave the PR ready - #97

Merged
JacobStephens2 merged 2 commits into
mainfrom
issue-93
Sep 27, 2026
Merged

JacobStephens2 merged 2 commits into
mainfrom
issue-93

Conversation

@JacobStephens2

Copy link
Copy Markdown
Owner

Summary

In a Merge run, a failed Self-merge now goes back round the Repair loop, with the same budgets, and is tried again on the new green head. If that round finds nothing to fix, it is a Policy refusal.

 implement
   run_session("implement")
   watched = repair_loop.run()
-  ensure_pr_ready_and_mergeable
-  if Merged: github::merge(watched)?          # any failure → Failed run, draft
+  loop
+    ensure_pr_ready_and_mergeable
+    if ReadyForReview or github::merge(watched) ok → done
+    round_after_failed_merge(watched)
+      origin/<base> moved since? → count a base move
+      repair_loop.run()                        # conflict / CI-fix Repairs, same budgets
+    NewHead(head)  → watched = head; try again
+    NothingToFix   → Err(merge error + PolicyRefusal)

failed_run::fail sees a PolicyRefusal and does two things:

  • It leaves the PR ready for review, not a draft.
  • It pushes no failure commit, so the PR stays on the head whose CI thirdshift watched.

The Run exits 1, prints the PR URL on stdout and puts GitHub's error on stderr. thirdshift never parses GitHub's error text: "nothing to fix" means the round ended on the same head with no Repair and no base move.

What else changed:

  • Shared budgets: they live in one Budgets struct inside RepairLoop, so rounds and Repairs after a failed merge count against the Run's existing caps.
  • Error message: the base-move one now reads origin/<base> kept moving: merged it again 5 times, because it covers both causes.
  • Fake gh: three new hooks:
    • gh fake refuse-merges <times> '<error>'
    • gh fake on-merge <times> '<script>', which moves the Base branch at merge time.
    • $FAKE_CI_SHA for on-ci-read scripts.
  • Docs: README documents the Merge run outcomes and the exception in Failed runs. CONTEXT.md defines Policy refusal.

Evidence

  • Before: there were 5 new tests in tests/merge_run.rs, and all 5 failed on the old code.
    After: the full suite passes (cargo test), and so does cargo clippy --all-targets.
merge fails on a Base branch conflict       → conflict Repair → merged
merge fails, red CI on new head             → CI-fix Repair → merged on the fix's head
6 failed merges, base moving each time      → "origin/main kept moving: merged it again 5 times", draft
5 CI-fix Repairs, then a conflict at merge  → "repairs exhausted: conflict", draft
merge refused, nothing to fix               → exit 1, URL on stdout, GitHub error on stderr,
                                              PR open + ready, issue-7 head == merged-attempted head

I checked the last assertion (no failure commit pushed on a refusal) both ways: it fails without the failed_run.rs change and passes with it.

Merge Danger

Door: two-way

Blast Radius: Merge-runs

  • Only the Merge run path changes. A plain Run gets the same Repair loop, except for the reworded base-move error.
  • A retried merge can spend more Repair sessions, but only within the existing caps of 5 Repairs and 5 base moves.

Unaddressed findings

Standards

  • PolicyRefusal is found through anyhow's context downcast, not through fail's signature. I kept it. It is a documented public type in failed_run, and fail's doc names it. Threading an outcome enum through run would widen every error path for a single case.
  • Move the merge-and-retry loop into RepairLoop. Skipped. implement owns the Run's goal (ready vs merged), and RepairLoop stays goal-agnostic.
  • "Nothing to fix" is worked out by comparing snapshots of Budgets. Kept. A Repair or a base move is exactly what the check means, and comparing the counters states that directly.
  • Data Clump / Middle Man: RepairLoop::run destructures self into locals. Kept. The locals are for readability, and the struct is there to carry the budgets across rounds.
  • The helpers in tests/merge_run.rs duplicate those in tests/ci.rs and tests/mergeable.rs. Skipped. That pattern already exists; a shared test-support refactor is out of scope here.

Spec

Closes #93

🤖 Generated with Claude Code

JacobStephens2 and others added 2 commits September 27, 2026 13:04
A Merge run whose merge fails now goes round again, merging the Base
branch, repairing conflicts and red CI within the Run's budgets, and
retries the merge on the new green head. A round that finds nothing to
fix is a policy refusal: the Run fails but leaves the PR ready for
review. The fake gh can refuse merges and run a script at merge time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A policy refusal no longer pushes an empty failed-run commit, which
left the ready PR on a head whose CI nobody watched. The round after a
failed merge now returns a Round enum, and CONTEXT.md defines Policy
refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JacobStephens2
JacobStephens2 merged commit 1b7e26f into main Sep 27, 2026
13 checks passed
@JacobStephens2
JacobStephens2 deleted the issue-93 branch September 27, 2026 13:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A failed Self-merge goes back round the Repair loop; policy refusals leave the PR ready

1 participant