Skip to content

Surface rebase recovery info in the merge-conflict prompt#225

Merged
subsetpark merged 11 commits into
mainfrom
zax/conflict-prompt-recovery-info
Apr 30, 2026
Merged

Surface rebase recovery info in the merge-conflict prompt#225
subsetpark merged 11 commits into
mainfrom
zax/conflict-prompt-recovery-info

Conversation

@subsetpark
Copy link
Copy Markdown
Collaborator

@subsetpark subsetpark commented Apr 30, 2026

Summary

  • Patch agents that hit a merge conflict now receive the exact git rebase --onto command, the patch's unique-commit list, and a git reset --hard <orig_head> emergency recovery anchor — so an agent that aborts the in-progress rebase can reconstruct the correct restart instead of guessing.
  • Recovery section also instructs git fetch origin first and uses origin/<base> as the rebase target in both delivery paths (fresh-rebase + rebase-already-in-progress), so the agent never operates against a stale local tracking ref.
  • Rebase decision logic decomposed into a pure layer (classify_unique_commits, parse_rebase_merge_state) and effectful handlers (rebase_onto, read_in_progress_conflict_info), following the existing classify_fetch_result / fetch_origin split.

Background

The orchestrator's auto-rebase already computes --onto arguments via find_old_base and uses them correctly when running git rebase --onto target old_base. But on conflict, the prompt only warned don't run git rebase origin/<base> — without telling the agent which command WOULD be correct. If the agent aborted mid-resolution (a common path when stuck), they had no way to reconstruct the right restart and would re-pick already-merged dependency commits. This was a real failure mode observed during a recent patch-4 conflict where the agent's local view of main was stale.

What changed

Decision layer (lib/worktree.ml)

  • New unique_commit, rebase_strategy, conflict_info types. Conflict now carries the payload (was a bare constructor).
  • classify_unique_commits ~project_name ~ancestor_ids log_output — pure: returns (unique_commit list * oldest_sha) from git log %H %s output, with the ancestor-subject filter applied. Existing oldest_non_ancestor_commit kept as a back-compat wrapper.
  • parse_rebase_merge_state — pure: assembles conflict_info from .git/rebase-merge/{onto,orig-head} contents + git log onto..orig-head output.

Effectful handlers

  • rebase_onto restructured to capture orig_head (HEAD before rebase runs) and emit Conflict { target; old_base; unique_commits; strategy; orig_head } on conflict. Plain-strategy fallback used when find_old_base couldn't isolate unique commits.
  • read_in_progress_conflict_info — new effectful helper that reconstructs conflict_info for the rebase-already-in-progress path. Best-effort: degrades to None (no recovery section) on any read/parse failure.

Prompt (lib/prompt.ml)

  • render_merge_conflict_prompt gains ?conflict_info. New "Recovery" section with git fetch origin, the exact git rebase --onto origin/<base> <old_base> command, oldest-first commit bullets, and a git reset --hard <orig_head> block when orig_head is populated. Plain-strategy renders an analogous block with git rebase origin/<base> and no commit list.
  • New template vars (recovery_section, old_base, target_branch, orig_head) for prompt overrides.

Plumbing (bin/main.ml)

  • deliver_to_agent gains ?conflict_info. Both call paths populate it: fresh-rebase binds Conflict ci; in-progress reads via read_in_progress_conflict_info.
  • In-progress path now passes target = origin/<base> (was bare <base>), matching the fresh-rebase path's behavior.

Tests

Pure decision layer (test/test_rebase_onto.ml, +10 properties):

  • classify_unique_commits properties: empty → Error; length pairs oldest; all-ancestor → Error; kept count + ancestor count = total; subject-with-spaces; CRLF; back-compat wrapper round-trip
  • parse_rebase_merge_state properties: well-formed → Some Onto with N commits; orig_head whitespace-stripped; blank onto → None; empty log → None

Prompt rendering (test/test_prompt_recovery.ml, new): legacy byte-equal regression (no Recovery section when ?conflict_info:None); Onto strategy renders fetch + exact --onto origin/main <old_base> + git reset --hard <orig_head> + commits oldest-first; Plain strategy renders fetch + git rebase origin/<base> + no --onto + no reset (orig_head empty).

State-machine interleavings (test/test_conflict_recovery_state_machine.ml, new): 500-step random command sequences over Idle → Auto_rebased → Conflict_delivered → Resolved/Aborted_then_recovered/Terminal. Invariants R-1..R-5: Onto strategy implies non-empty old_base + commits; Onto delivery cannot transition via Agent_recovers_with_plain (R-2); reconstruction round-trips commit set; sequences ending in recovery commands reach Terminal.

All ~155 properties + 10 git-repo integration tests pass; pre-commit hooks (build + test + format) clean.

Test plan

  • dune build clean (fatal warnings)
  • dune runtest green
  • dune fmt clean
  • Pre-commit hook (build + test + format-check) passes
  • Smoke test in a real conflict scenario: trigger a patch with a merged-dependency conflict, abort the rebase, confirm the agent's prompt contains the correct git rebase --onto origin/main <sha> command and orig_head reset anchor
  • Verify in-progress reconstruction: kill orchestrator mid-rebase, restart, confirm reconstructed prompt matches the fresh-rebase prompt

🤖 Generated with Claude Code


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

Summary by CodeRabbit

  • New Features

    • Enhanced rebase conflict recovery with detailed recovery instructions in conflict prompts
    • Captures and preserves critical recovery context during rebases
    • Provides strategy-specific guidance for conflict resolution workflows
  • Bug Fixes

    • Improved handling of in-progress rebases with better state reconstruction
  • Tests

    • Added comprehensive test coverage for conflict recovery scenarios

When the orchestrator's auto-rebase hits conflicts and hands off to the
patch agent, the prompt previously warned "do not run `git rebase
origin/<base>`" without telling the agent which command WOULD be correct
if the in-progress rebase state was lost. The orchestrator already
computed the `--onto` arguments via `find_old_base` but did not forward
them, so an agent that aborted resolution couldn't reconstruct the right
restart command.

Decompose the rebase logic into a pure decision layer
(`classify_unique_commits`, `parse_rebase_merge_state`) and effectful
handlers (`rebase_onto`, `read_in_progress_conflict_info`), following
the existing `classify_fetch_result` / `fetch_origin` split. Extend
`Conflict` with a `conflict_info` payload carrying the target branch,
old_base SHA, patch-unique commit list, strategy, and pre-rebase HEAD.
Thread it through both delivery paths: fresh-rebase (binds `Conflict
ci`) and rebase-already-in-progress (reads
`.git/rebase-merge/{onto,orig-head}` and runs `git log onto..orig-head`
against `origin/<base>`).

The conflict prompt now renders a "Recovery" section with `git fetch
origin`, the exact `git rebase --onto origin/<base> <old_base>` command,
the unique-commit list (oldest first), and a `git reset --hard
<orig_head>` emergency-recovery anchor when capture succeeded. Plain-
strategy fallback (no unique commits isolated) renders a similar block
with `git rebase origin/<base>` and no commit list.

Decision layer is exhaustively property-tested in
test_rebase_onto.ml (10 single-shot properties incl. ordering,
ancestor-filter, CRLF, back-compat wrapper round-trip,
parse_rebase_merge_state orig_head stripping). Prompt rendering
asserted in test_prompt_recovery.ml. State-machine interleavings in
test_conflict_recovery_state_machine.ml verify R-1..R-5 over 500-step
random command sequences (Onto delivery cannot accept Plain recovery,
reconstruction round-trips commit set, etc.).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
onton Ready Ready Preview, Comment Apr 30, 2026 2:43pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9ee23eb3-25b5-49ef-b1d6-538dc46ec664

📥 Commits

Reviewing files that changed from the base of the PR and between fa5045f and 3bbc6af.

📒 Files selected for processing (4)
  • lib/prompt.ml
  • lib/worktree.ml
  • lib/worktree.mli
  • test/test_conflict_recovery_state_machine.ml

📝 Walkthrough

Walkthrough

This pull request refactors merge-conflict handling by converting Worktree.Conflict from a payload-less constructor to Conflict of conflict_info, adding structured recovery context including target branch, old base, unique commits, strategy, and original HEAD. The conflict info is threaded through the orchestration pipeline and used to render an enhanced merge-conflict prompt with a "Recovery" section containing exact git commands for resuming the rebase.

Changes

Cohort / File(s) Summary
Conflict info type system
lib/worktree.ml, lib/worktree.mli
Introduces unique_commit, rebase_strategy, and conflict_info types; refactors rebase_result to Conflict of conflict_info; adds functions to classify commits, parse rebase-merge state, and read in-progress conflict info for recovery.
Orchestrator pattern updates
lib/orchestrator.ml
Updates pattern matches for Worktree.Conflict to accept payload (Worktree.Conflict _) in apply_rebase_result and apply_conflict_rebase_result.
Prompt recovery section
lib/prompt.ml, lib/prompt.mli
Adds render_recovery_section function; extends render_merge_conflict_prompt with optional ?conflict_info parameter to inject recovery instructions, git commands, and commit listings into merge-conflict prompts.
Main orchestration logic
bin/main.ml
Threads optional conflict_info through deliver_to_agent and merge-conflict paths; captures conflict payload from rebase results and fetches enriched conflict context for already-in-progress rebases.
Test configuration
test/dune
Adds two new test executables: test_prompt_recovery and test_conflict_recovery_state_machine with QCheck2 dependencies.
Test coverage
test/test_prompt_recovery.ml, test/test_rebase_onto.ml, test/test_conflict_recovery_state_machine.ml
Adds comprehensive tests: prompt recovery assertions (legacy regression, Onto/Plain strategy validation), commit classification and rebase-merge state parsing properties, and a phase-based state machine model for conflict recovery lifecycle with invariant checks.
Test stub updates
test/test_orchestrator_properties.ml, test/test_interleaving_properties.ml
Updates test properties to construct Worktree.Conflict with explicit stub_conflict_info payload instead of nullary constructor.

Sequence Diagram

sequenceDiagram
    participant Main as Main
    participant Worktree as Worktree
    participant Orchestrator as Orchestrator
    participant Prompt as Prompt
    participant Agent as Agent

    Main->>Worktree: rebase_onto()
    Worktree->>Worktree: capture orig_head<br/>compute old_base<br/>classify unique_commits
    alt Rebase fails
        Worktree-->>Main: Conflict of conflict_info
    else Rebase in progress
        Main->>Worktree: read_in_progress_conflict_info()
        Worktree-->>Main: enriched conflict_info
    end

    Main->>Orchestrator: apply_rebase_result<br/>(conflict_info)
    Orchestrator->>Main: deliver_to_agent<br/>(?conflict_info)

    Main->>Prompt: render_merge_conflict_prompt<br/>(?conflict_info)
    alt conflict_info present
        Prompt->>Prompt: render_recovery_section<br/>(unique_commits,<br/>old_base, orig_head)
        Prompt-->>Main: prompt with<br/>Recovery section
    else conflict_info absent
        Prompt-->>Main: legacy prompt<br/>(unchanged)
    end

    Main->>Agent: deliver(prompt)
    Agent-->>Main: recovery command
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 A conflict once bare, now richly adorned,
With conflict_info born, recovery sworn.
Through worktree and prompt, the recovery flows,
Git's path to restart—now plainly shown!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Surface rebase recovery info in the merge-conflict prompt' accurately and specifically describes the main change: adding recovery information to the merge-conflict prompt that users receive during rebases.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch zax/conflict-prompt-recovery-info

Review rate limit: 0/5 reviews remaining, refill in 57 minutes and 12 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@flowglad-review-service flowglad-review-service Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: 3 comments

Comment thread lib/worktree.ml
Comment thread lib/worktree.ml Outdated
Comment thread bin/main.ml
@flowglad-review-service
Copy link
Copy Markdown

Review Summary

The PR is well-structured and the core logic is sound. Three findings, all in the newly added code:

  1. Should-fix — file descriptor leak (lib/worktree.ml:748): read_file_opt does not close the channel when really_input raises. The surrounding try … with _ -> None catches the exception but skips close_in, leaking one fd per failed read. Fix with Fun.protect ~finally:close_in_noerr.

  2. Should-fix — silent invariant in parse_rebase_merge_state (lib/worktree.ml:466): The function always returns strategy = Onto. This is correct today because plain git rebase uses .git/rebase-apply/, making the function unreachable for plain rebases under default git configuration. However, rebase.backend = merge in a user's gitconfig forces .git/rebase-merge/ even for a plain rebase, which would produce an empty old_base and a broken recovery command. A comment documenting the assumption (and the exception) would prevent a future regressor.

  3. Should-fix — intent invisible at re-deliver site (bin/main.ml:3328): At the Conflict_needs_agent arm, conflict_info can be None (when rebase was Ok/Noop and only the push failed), causing the re-delivered prompt to lack a recovery section. The code is correct but the reason is invisible; a comment would prevent a reader from incorrectly assuming conflict_info should always be Some on this path.
    | Severity | Count |
    |----------|-------|
    | Must-fix | 0 |
    | Should-fix | 3 |
    | Note | 0 |


3 comments posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 194649 in / 8406 out · Cache: 50008 created / 408312 read · Review mode: agentic · Turns: 15 · Tool calls: 22 · Tool mix: read_file=19, search_codebase=2, submit_review=1

Wrap the channel in Fun.protect so close_in always runs on the
truncated/short-read path, document why parse_rebase_merge_state
emits strategy = Onto unconditionally, and explain the
None-conflict_info degraded path at the Conflict_needs_agent arm.
Copy link
Copy Markdown

@flowglad-review-service flowglad-review-service Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: 2 comments

Comment thread lib/worktree.ml Outdated
Comment thread lib/prompt.ml Outdated
@flowglad-review-service
Copy link
Copy Markdown

Review Summary

Two findings:

  1. must-fixparse_rebase_merge_state sets old_base = String.strip onto_contents, but .git/rebase-merge/onto holds the --onto target SHA, not the upstream/old-base SHA. The upstream (second positional argument to git rebase --onto) lives in .git/rebase-merge/upstream. The bug means the recovery command embedded in the agent prompt has both arguments set to the same target SHA, which would replay commits onto themselves rather than excluding already-merged ancestors. The fix requires reading .git/rebase-merge/upstream for old_base.

  2. should-fixrender_recovery_section for the Onto strategy unconditionally emits "Commits unique to this patch (oldest first):" even when unique_commits = []. While production invariants prevent this, the type does not enforce it, so a render-time guard would prevent a confusing dangling header in edge cases.
    | Severity | Count |
    |----------|-------|
    | Must-fix | 1 |
    | Should-fix | 1 |
    | Note | 0 |


2 comments posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 142673 in / 7097 out · Cache: 73103 created / 429269 read · Review mode: agentic · Turns: 15 · Tool calls: 17 · Forced submit: yes · Tool mix: read_file=14, search_codebase=3

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 13 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="test/test_conflict_recovery_state_machine.ml">

<violation number="1" location="test/test_conflict_recovery_state_machine.ml:254">
P2: `prop_eventually_terminal_when_finished` is currently vacuous: both `Terminal` and `Active` outcomes return `true`, so it does not verify the claimed R-5 termination behavior.

(Based on your team's feedback about avoiding vacuous passes in state-machine property tests.) [FEEDBACK_USED]</violation>
</file>

<file name="lib/worktree.ml">

<violation number="1" location="lib/worktree.ml:470">
P1: In in-progress conflict reconstruction, `old_base` is set from `.git/rebase-merge/onto` (the new base) instead of the upstream anchor, so the recovery `git rebase --onto <target> <old_base>` command can replay the wrong commit range.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Comment thread lib/worktree.ml Outdated
Comment thread test/test_conflict_recovery_state_machine.ml Outdated
parse_rebase_merge_state was reading .git/rebase-merge/onto and using
its SHA as old_base, but onto holds the rebase destination, not the
upstream. The recovery command was therefore git rebase --onto X X
with the same SHA twice. Now read .git/rebase-merge/upstream as well
and use its stripped value as old_base; range upstream..orig-head also
matches the rebase's actual replay set.

In render_recovery_section, build commits_section conditionally so an
empty unique_commits list no longer renders a dangling header.
Copy link
Copy Markdown

@flowglad-review-service flowglad-review-service Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: 2 comments

Comment thread lib/worktree.mli Outdated
Comment thread lib/worktree.ml
@flowglad-review-service
Copy link
Copy Markdown

Review Summary

The diff is well-structured and the major onto/upstream correctness bug from the prior review round has been fixed. Two new should-fix issues were found:

  1. lib/worktree.mli:134 — The conflict_info doc says "oldest-last" for unique_commits but the actual ordering convention (git-log newest-first, reversed by the renderer) is ambiguous. No assertion enforces it, so a caller constructing the struct with oldest-first commits would silently produce an inverted bullet list.

  2. lib/worktree.ml:463parse_rebase_merge_state returns None when the ancestor filter eliminates all commits from the log, but rebase_onto emits Conflict { strategy = Plain; ... } on the same state. An agent that hits this state after an orchestrator restart (via read_in_progress_conflict_info) gets no recovery section, while the original delivery gave a Plain-strategy section — a silent regression in restart quality.
    | Severity | Count |
    |----------|-------|
    | Must-fix | 0 |
    | Should-fix | 2 |
    | Note | 0 |


2 comments posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 154737 in / 4225 out · Cache: 85763 created / 461420 read · Review mode: agentic · Turns: 15 · Tool calls: 18 · Forced submit: yes · Tool mix: read_file=17, search_codebase=1

The previous prop_eventually_terminal_when_finished returned true on
both `Terminal and `Active outcomes, so it could not detect a model
where a precondition-holding final command failed to reach a terminal
state. Split out a precondition_holds predicate (Auto_rebased +
Auto_resolve, or Conflict_delivered + matching strategy on
Agent_recovers_*) and require Terminal-equivalence only in that arm;
no-op tail commands from non-eligible states still pass via the
`Active, false branch.
Copy link
Copy Markdown

@flowglad-review-service flowglad-review-service Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: 1 comment

Comment thread lib/worktree.mli Outdated
@flowglad-review-service
Copy link
Copy Markdown

Review Summary

One new issue found in this incremental diff: the .mli doc comment for read_in_progress_conflict_info (introduced by this PR) describes the wrong file set ({onto,orig-head}) and the wrong git-log range (onto..orig-head), while the implementation reads {onto,upstream,orig-head} and uses upstream..orig-head. The upstream file and upstream-bounded range are the key correctness mechanism; a mismatched doc creates a misleading contract for future readers and callers. All outstanding comments from prior review turns remain unaddressed; no new instances of those patterns appear in this diff.

Severity Count
Must-fix 0
Should-fix 1
Note 0

1 comment posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 150692 in / 3317 out · Cache: 79813 created / 468432 read · Review mode: agentic · Turns: 15 · Tool calls: 17 · Forced submit: yes · Tool mix: read_file=16, search_codebase=1

parse_rebase_merge_state now returns Some when classify_unique_commits
errors but onto/upstream are valid SHAs: the recovery command is
fully determined by target and old_base, so a restarted orchestrator
should still surface it. unique_commits is empty in that case; the
prompt renderer (changed in b6a52ea) omits the bullet header.

Also clarify the conflict_info doc on unique_commits ordering: head
of the list is the newest commit, last element is the oldest, and the
prompt reverses for display. Two new property tests pin the new
empty-commits-but-valid-command behavior.
The .mli still described reading .git/rebase-merge/{onto,orig-head}
and running git log onto..orig-head; the implementation in 6a2ab5f
reads {onto,upstream,orig-head} and runs git log upstream..orig-head.
Sync the doc.
Copy link
Copy Markdown

@flowglad-review-service flowglad-review-service Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: 2 comments

Comment thread lib/worktree.mli
Comment thread test/test_conflict_recovery_state_machine.ml
@flowglad-review-service
Copy link
Copy Markdown

Review Summary

Two issues found in code introduced by this diff:

  1. lib/worktree.mli:197 (should-fix) — The .mli doc comment for read_in_progress_conflict_info still describes the old two-file / onto..orig-head form. The PR's implementation reads three files and uses upstream..orig-head. This was flagged in turn 4 and remains unaddressed.

  2. test/test_conflict_recovery_state_machine.ml:131 (should-fix) — R-1 requires not (List.is_empty ci.unique_commits) for Onto. Production code allows Onto with an empty commit list (confirmed by prop_empty_log_keeps_command and prop_all_filtered_keeps_command). The model generator never exercises that case so the test passes, but the invariant misrepresents the actual contract and will produce a false failure in any future extension.
    | Severity | Count |
    |----------|-------|
    | Must-fix | 0 |
    | Should-fix | 2 |
    | Note | 0 |


2 comments posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 160049 in / 4367 out · Cache: 84477 created / 490556 read · Review mode: agentic · Turns: 15 · Tool calls: 17 · Forced submit: yes · Tool mix: read_file=15, search_codebase=2

production parse_rebase_merge_state can produce
{ strategy = Onto; unique_commits = []; old_base = <upstream> }
when the log is empty or every commit is ancestor-filtered (covered
by prop_empty_log_keeps_command and prop_all_filtered_keeps_command
in test_rebase_onto.ml). The model's R-1 was stricter than the type's
contract, which would spuriously fail any future model extension that
threaded a reconstructed conflict_info through apply.
Copy link
Copy Markdown

@flowglad-review-service flowglad-review-service Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: 2 comments

Comment thread lib/prompt.ml
Comment thread test/test_conflict_recovery_state_machine.ml
@flowglad-review-service
Copy link
Copy Markdown

Review Summary

The diff is overall well-structured and the previously flagged correctness issues (upstream vs onto, fd leak, doc mismatches) are now addressed or outstanding. Two new findings in the newly introduced code:

  1. render_recovery_section Onto arm (lib/prompt.ml:758): The "do NOT run" warning substitutes base_branch while the actual command substitutes ci.target. Today both are "origin/main" because callers always pass "origin/" ^ base, so they agree accidentally — but they are driven by separate variables, which creates a latent divergence risk. Fixing both to use ci.target removes the implicit invariant.

  2. State-machine onto_fixture.target (test/test_conflict_recovery_state_machine.ml:46): Uses "main" (bare) rather than the "origin/main" format that production always produces. The test therefore never catches a regression involving ci.target formatting in the rendered prompt command.
    | Severity | Count |
    |----------|-------|
    | Must-fix | 0 |
    | Should-fix | 2 |
    | Note | 0 |


2 comments posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 163027 in / 3988 out · Cache: 85791 created / 510200 read · Review mode: agentic · Turns: 15 · Tool calls: 19 · Forced submit: yes · Tool mix: read_file=15, search_codebase=4

The Onto recovery section used base_branch for the warning sentence
('do NOT run git rebase origin/<base_branch>') and ci.target for the
actual rebase command. They happened to agree in production
(ci.target = origin/<base_branch>), but the relationship was implicit
and would diverge silently if a caller ever constructed conflict_info
with a different format. Use ci.target in both spots so the warning
and the command can never disagree; ~base_branch is no longer needed
by render_recovery_section, so drop it (the public
render_merge_conflict_prompt API is unchanged).

Also align onto_fixture and plain_fixture in the state-machine model
to use 'origin/main' (matching production), so future model
extensions threading these fixtures through render_recovery_section
exercise the same target shape the agents actually see.
Copy link
Copy Markdown

@flowglad-review-service flowglad-review-service Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NOTE] Review pipeline encountered an error.

Copy link
Copy Markdown

@flowglad-review-service flowglad-review-service Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: 1 comment

Comment thread test/test_conflict_recovery_state_machine.ml
@flowglad-review-service
Copy link
Copy Markdown

Review Summary

The incremental diff successfully addresses the outstanding issues from Turns 1–6: read_file_opt now uses Fun.protect to close the fd on all paths; parse_rebase_merge_state correctly reads upstream_contents as old_base (not onto); the commits_section guard prevents a dangling header when unique_commits is empty; the .mli doc comment for read_in_progress_conflict_info accurately describes the three files and the upstream..orig-head range; R-1 in the state machine permits empty unique_commits for Onto strategy; render_recovery_section uses ci.target consistently for both the warning and the command; and onto_fixture.target is "origin/main" to match production call sites.\n\nOne new issue in this diff: prop_eventually_terminal_when_finished returns false when the core sequence has an invariant error (prev_err = Some _), which conflates an R-1..R-4 violation with an R-5 falsification. The fix is to return true on that path (treating it as a precondition failure, not a test failure), consistent with the QCheck2 convention documented in CLAUDE.md.

Severity Count
Must-fix 0
Should-fix 1
Note 0

1 comment posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 181000 in / 4243 out · Cache: 90477 created / 535237 read · Review mode: agentic · Turns: 15 · Tool calls: 19 · Forced submit: yes · Tool mix: read_file=16, search_codebase=3

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/test_prompt_recovery.ml (1)

95-135: ⚡ Quick win

Add the degraded Onto/empty-commit recovery case.

This file covers Onto with commits and Plain with no commits, but the new behavior in this PR also allows strategy = Onto with unique_commits = []. A renderer regression that treats every empty list like Plain would still pass here. Please add an explicit assertion that Onto still renders git rebase --onto <target> <old_base> while simply omitting the commit bullets.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/test_prompt_recovery.ml` around lines 95 - 135, Add a new test case
similar to the existing Plain one that constructs a Worktree.conflict_info with
strategy = Onto and unique_commits = [] (and appropriate old_base and target
like "origin/release"/"release"), render the prompt via
Prompt.render_merge_conflict_prompt, slice out the "## Recovery (if rebase state
is lost)" section, and assert that the recovery_section contains the exact
rebase form "git rebase --onto <target> <old_base>" (e.g., "git rebase --onto
origin/release <old_base>") while also asserting that there are no per-patch
commit bullets (reuse assert_contains/assert_not_contains helpers to check
presence of the --onto rebase command and absence of any commit list bullets).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/prompt.mli`:
- Around line 64-68: Update the docstring for the merge-conflict prompt function
to accurately describe strategy-dependent Recovery output: state that when
~conflict_info is provided the Recovery section contains a reconstruction hint
but its exact contents depend on the chosen strategy (e.g., the Plain strategy
renders a simple "git rebase <target>" path and may omit the per-commit bullet
list, whereas other strategies include the explicit "git rebase --onto" command
plus unique-commit bullets). Reference the existing parameters by name (e.g.,
~conflict_info and the Plain strategy) so readers can find the relevant behavior
in the merge-conflict prompt renderer.

---

Nitpick comments:
In `@test/test_prompt_recovery.ml`:
- Around line 95-135: Add a new test case similar to the existing Plain one that
constructs a Worktree.conflict_info with strategy = Onto and unique_commits = []
(and appropriate old_base and target like "origin/release"/"release"), render
the prompt via Prompt.render_merge_conflict_prompt, slice out the "## Recovery
(if rebase state is lost)" section, and assert that the recovery_section
contains the exact rebase form "git rebase --onto <target> <old_base>" (e.g.,
"git rebase --onto origin/release <old_base>") while also asserting that there
are no per-patch commit bullets (reuse assert_contains/assert_not_contains
helpers to check presence of the --onto rebase command and absence of any commit
list bullets).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dac09721-ca8a-4b72-9c07-ac90f8e38fdb

📥 Commits

Reviewing files that changed from the base of the PR and between f511d3e and fa5045f.

📒 Files selected for processing (13)
  • bin/main.ml
  • lib/orchestrator.ml
  • lib/prompt.ml
  • lib/prompt.mli
  • lib/worktree.ml
  • lib/worktree.mli
  • lib_test/test_generators.ml
  • test/dune
  • test/test_conflict_recovery_state_machine.ml
  • test/test_interleaving_properties.ml
  • test/test_orchestrator_properties.ml
  • test/test_prompt_recovery.ml
  • test/test_rebase_onto.ml

Comment thread lib/prompt.mli
If run_sequence on the core or final cmds reports an invariant error,
that's prop_invariants_hold's concern, not R-5's. Returning false
turned an R-1..R-4 violation into an R-5 counter-example that QCheck
would shrink toward the tail command — misleading. Treat both as
precondition failures and pass.
Copy link
Copy Markdown

@flowglad-review-service flowglad-review-service Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: 1 comment

Comment thread lib/worktree.ml Outdated
@flowglad-review-service
Copy link
Copy Markdown

Review Summary

One new issue found in this incremental diff. The read_in_progress_conflict_info function (new in this diff) includes orig_head in its emptiness guard and returns None when the orig-head file is absent or empty — discarding valid onto+upstream SHAs and losing the entire recovery section. This is inconsistent with both rebase_onto (which stores "" for orig_head on failure and still emits a Conflict with recovery info) and parse_rebase_merge_state (which accepts empty orig_head and stores it as-is). The fix removes orig_head from the early-return guard and uses HEAD as the log-range fallback when orig_head is empty. All outstanding comments from prior turns remain open and are not re-raised here.

Severity Count
Must-fix 0
Should-fix 1
Note 0

1 comment posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 216560 in / 4352 out · Cache: 89410 created / 321208 read · Review mode: agentic · Turns: 12 · Tool calls: 12 · Tool mix: read_file=12

Previously, a missing or empty .git/rebase-merge/orig-head dropped the
entire recovery section even though onto and upstream were valid. Mirror
the fresh-rebase path (which stores "" when git rev-parse HEAD fails and
still emits the conflict_info): only onto and upstream are required;
orig-head is best-effort. When orig-head is empty, fall back to
upstream..HEAD for the log range so the prompt still gets the rebase's
intended replay set.
Copy link
Copy Markdown

@flowglad-review-service flowglad-review-service Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: 1 comment

Comment thread lib_test/test_generators.ml Outdated
@flowglad-review-service
Copy link
Copy Markdown

Review Summary

The incremental diff addresses several prior comments (R-1 guard corrected, prop_eventually_terminal_when_finished precondition handling fixed, conflict_info doc comment improved). One new issue found: eight generator helpers added to lib_test/test_generators.ml are never referenced by any test file and risk fatal warning 32 (unused-value-declaration) at build time. They should either be wired up to a test or removed.

Severity Count
Must-fix 0
Should-fix 1
Note 0

1 comment posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 177777 in / 3116 out · Cache: 94150 created / 538275 read · Review mode: agentic · Turns: 15 · Tool calls: 21 · Forced submit: yes · Tool mix: read_file=15, search_codebase=6

gen_sha, gen_non_ancestor_subject, gen_unique_commit, log_body_of_commits,
gen_log_lines, gen_rebase_strategy, gen_target_branch, and
gen_conflict_info were added in this branch but no test imports them.
Drop them until a caller exists rather than carry dead generator code.
@flowglad-review-service
Copy link
Copy Markdown

Review Summary

This incremental diff is clean. It correctly addresses several previously-raised concerns: read_file_opt now uses Fun.protect to prevent fd leaks; parse_rebase_merge_state uses upstream_contents (not onto_contents) as old_base; render_recovery_section guards the commits header against the empty-list case; prop_eventually_terminal_when_finished returns true (not false) on invariant violations in the core sequence; and R-1 correctly allows Onto strategy with an empty unique_commits list. The Conflict _ wildcard updates throughout the call sites are mechanical and correct. No new correctness bugs, security issues, or must-fix problems are introduced by this diff. The outstanding comments from prior turns (doc-string mismatches in the .mli, the base_branch vs ci.target divergence in the Onto warning sentence, onto_fixture.target = "main" vs the production "origin/main" format, and the unused generators in test_generators.ml) remain live on the PR thread.

Severity Count
Must-fix 0
Should-fix 0
Note 0

0 comments posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 171293 in / 2734 out · Cache: 89050 created / 522370 read · Review mode: agentic · Turns: 15 · Tool calls: 16 · Forced submit: yes · Tool mix: read_file=16

@subsetpark subsetpark merged commit 73d76c0 into main Apr 30, 2026
10 checks passed
@subsetpark subsetpark deleted the zax/conflict-prompt-recovery-info branch April 30, 2026 14:54
@coderabbitai coderabbitai Bot mentioned this pull request May 24, 2026
8 tasks
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.

1 participant