Skip to content

Reliable stacked-PR rebase anchors#319

Merged
subsetpark merged 13 commits into
mainfrom
stacked-pr-rebase-anchors
May 24, 2026
Merged

Reliable stacked-PR rebase anchors#319
subsetpark merged 13 commits into
mainfrom
stacked-pr-rebase-anchors

Conversation

@subsetpark
Copy link
Copy Markdown
Collaborator

@subsetpark subsetpark commented May 24, 2026

Summary

Fixes the squash-merge stacked-PR rebase trap observed in production at ~/.local/share/onton/ts-lowerability-effect-boundary-checker (p4/p5). A patch branched off a dependency at Start, idle through the dep's squash-merge, would on its first rebase try to replay the dep's pre-squash commits on top of the post-squash main and conflict on every file the dep added.

Root cause: branch_rebased_onto_sha was recorded at exactly one site — successful Rebase. A patch branched-off-dep had no anchor at first-rebase time, fell back to the legacy 2-arg form, and conflicted.

The fix is structural, not a patch: records anchors at every base transition (Start, Ok/Noop rebase, Ok/Noop merge-conflict rebase), keeps a bounded history for divergence fallback, splits the decision into pure (Rebase_decision.plan) and effectful (executor + handler), and deletes the legacy upstream shim now that the planner is consulted directly.

Commit sequence (11 commits, each green on its own)

  1. Anchor + Anchor_history value modules (validation, dedup, cap 8) + property tests.
  2. Restructured Rebase_decision: plan + anchor_after_result with reason variants.
  3. anchor_history on Patch_agent + persistence round-trip.
  4. Worktree_plan gains Capture_anchor / Record_anchor_on_success ops and for_start.
    5.1 Runner Rebase path uses atomic Orchestrator.apply_rebase_with_anchor.
    5.2 Executor computes Rebase_decision.plan from anchor history; legacy Rebase_decision.upstream shim deleted; Worktree.rebase_onto takes ~upstream:string.
  5. Same wire-up for Merge_conflict path.
  6. for_start runs at Start before the LLM session — closes the production bug.
  7. Documented why refresh_base_branch deliberately preserves the anchor (drift detector relies on it; PI-16).
  8. State-machine interleaving tests (SM-1, SM-2 = production bug, SM-3a/b, SM-4, SM-7, SM-8, baseline).
  9. AGENTS.md anchor lifecycle section.

Test plan

  • dune build strict-warnings clean
  • dune runtest full suite green at every commit
  • New property suite test_rebase_decision_properties.ml (RD-PLAN-1..8, RD-AAR-1..5)
  • New unit + property suite test_anchor.ml (12 tests)
  • New state-machine suite test_rebase_state_machine.ml (8 named scenarios, production bug among them)
  • Existing test_rebase_onto.ml integration tests pass against the new ~upstream signature
  • test_persistence_properties.ml extended with anchor_history round-trip + missing-key default
  • Pre-existing PI-16 drift detector test still passes (step 8 explicitly preserves its semantics)

In-flight broken agents (p4/p5 in production) are out of scope per prior discussion — they will be unstuck manually.

🤖 Generated with Claude Code


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


Summary by cubic

Makes stacked-PR rebases reliable by recording and using rebase anchors to compute safe git --onto upstreams. This stops replaying dependency commits after squash-merges and removes “both added” conflicts.

  • New Features

    • Record anchors at Start and after Ok/Noop rebase and merge-conflict flows; keep a capped, deduped Anchor_history.
    • New pure planner (Rebase_decision.plan) picks the best upstream from history using reachability (Worktree.is_ancestor), with clear reasons and fallbacks.
    • Executor captures anchors via Worktree_plan ops (Capture_anchor/Record_anchor_on_success) and returns anchor events; orchestrator applies rebase + anchor updates atomically (apply_rebase_with_anchor). Start uses Worktree_plan.for_start to capture the initial anchor.
    • Worktree.rebase_onto now requires ~upstream; the legacy upstream shim is removed. refresh_base_branch intentionally preserves anchors for drift detection.
    • Docs: added AGENTS.md section explaining the anchor lifecycle.
  • Bug Fixes

    • Fixes the squash-merge trap in stacked PRs: dependent branches no longer replay a dependency’s pre-squash commits onto post-squash main.
    • Corrects the plain rebase fallback so the executor uses the correct target ref as upstream when no anchor is available.

Written for commit f97057a. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • New Features
    • Added anchor tracking: the system now records the base branch and commit SHA when patches are created or rebased, storing up to 8 most recent anchors.
    • Improved rebase decision-making using recorded anchor history for more stable upstream selection.
    • Anchor information is now automatically captured at patch creation, rebase, and merge-conflict resolution points.

Review Change Stack

subsetpark and others added 11 commits May 24, 2026 12:39
Foundation for the stacked-PR rebase anchor work. Anchor is a recorded
fact (base, sha, observed_at_remote) with a 40-char lowercase-hex smart
constructor; Anchor_history is a newest-first bounded list (cap 8) that
dedups by (base, sha) on push and truncates over-cap legacy snapshots
on load. No call sites are touched in this commit — pure additions.

Property tests in test/test_anchor.ml cover SHA validation, accessor
round-trip, yojson round-trip on both modules, push monotonicity / cap /
dedup / newest-at-head, and over-cap truncation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a richer pure decision interface on top of the existing single-SHA
upstream shim:

  - type input { anchor; recorded_history; base_branch; head_sha }
  - type plan = Onto { target; upstream; reason } | Plain { target; reason }
  - type reason = Anchor_matches_head | History_fallback of string |
                  No_anchor | Anchor_unreachable_from_head | Head_unobservable
  - val plan : input -> ancestor_oracle:(...) -> plan
  - val anchor_after_result : prev:... -> result:Worktree_parser.rebase_result
                              -> resolved_remote_sha:... -> base_branch:...
                              -> Anchor.t option

plan picks the safest upstream by checking, in order, whether each anchor
(newest first, then history) is reachable from the patch's HEAD via a
caller-supplied ancestor_oracle. The newest reachable anchor wins
(Anchor_matches_head); an older history entry is the fallback when the
newest is unreachable (History_fallback). Plain is returned only when no
anchor is reachable (or HEAD is unobservable).

anchor_after_result encapsulates the three-blind-spot policy: refresh on
Ok and Noop when a remote SHA was resolved; preserve the prior anchor on
Conflict and Error so a failed attempt never erases prior knowledge.

Existing upstream shim is unchanged and still consumed by
Worktree.rebase_onto. Step 5 wires plan into the runner fiber and removes
the shim.

Property tests cover RD-PLAN-1..8 and RD-AAR-1..5, including the
production squash-merge scenario (RD-PLAN-8) — anchor recorded at Start
of a stacked patch, dep squash-merges, plan must produce
Onto { upstream = old_dep_tip }.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the anchor_history : Anchor_history.t field to Patch_agent.t with
two new smart setters:

  - record_anchor : t -> Anchor.t -> t
      Pushes onto the history (newest-first, dedup, capped at 8) AND
      mirrors the anchor into the legacy branch_rebased_onto /
      branch_rebased_onto_sha view fields so existing readers (drift
      detector, persistence writer) continue to see a coherent single-
      anchor view during the migration.

  - clear_anchor_for_base : t -> Branch.t -> t
      Used by the orchestrator when retargeting a base (next step) —
      preserves history so Rebase_decision.plan can still fall back to
      older entries, but clears the legacy SHA which would otherwise
      pin an obsolete value.

Persistence writes a new "anchor_history" key; missing key on load
defaults to Anchor_history.empty so older snapshots round-trip
identically. The new field threads through Patch_agent.restore at every
caller (test_patch_agent, test_patch_controller, test_poll_log,
persistence) — the codebase convention is explicit labeled args, not
optional defaults.

Property tests added to test_persistence_properties.ml:

  - anchor_history_roundtrip — non-empty histories survive
    yojson_of_t / patch_agent_of_yojson identically.
  - missing_anchor_history_defaults_empty — legacy snapshots without
    the key load with empty history (back-compat).

Also fixes a non-fatal doc-comment markup warning in
test_rebase_decision_properties.ml from step 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two new ops to the plan vocabulary:

  - Capture_anchor of { ref_name; slot }
      Resolves ref_name (e.g. "origin/main") to a SHA via
      Worktree.read_branch_sha and stores it in slot.
  - Record_anchor_on_success of { slot; base }
      Emits a Worktree_plan.anchor_event from slot's captured SHA iff
      the most recent Rebase_onto returned Ok or Noop (or no rebase ran).

The new event type Worktree_plan.anchor_event = Anchor_recorded of
Anchor.t | Anchor_capture_failed is the executor's side-channel for
telling the runner fiber which anchors to fold into agent state.

Adds Worktree_plan.for_start ~base = [Ensure_worktree; Fetch_origin;
Capture_anchor; Record_anchor_on_success]. No Rebase_onto — this plan
just captures and records an initial anchor for freshly-branched-off
patches. Step 7 wires it into the runner Start path, closing the
production-bug blind spot.

Executor changes:

  - Maintains a sparse int -> string option slot table for the plan.
  - Returns (rebase_result, path, anchor_event list) instead of
    (rebase_result, path). Existing callers in runner_fiber_impl.ml
    accept the new third element with _anchor_events (consumed in
    step 5+).
  - Conflict/Error short-circuit (Record_anchor_on_success after a
    failed rebase does NOT fire); Ok and Noop both continue (Noop
    DOES refresh the anchor, matching anchor_after_result's policy).
  - Default last_rebase = Ok so for_start plans without any rebase
    still record their anchor.

Plan-validity property tests extended for the new variants;
for_rebase / for_merge_conflict are unchanged in this commit — step
5/6 update them to include the new ops when the runner is rewired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
for_rebase now emits anchor events around its Rebase_onto:

  Ensure_worktree -> Fetch_origin -> Capture_anchor {origin/<new_base>, slot=0}
  -> Rebase_onto origin/<new_base> -> Record_anchor_on_success {slot=0, new_base}

The executor emits Anchor_recorded on Ok/Noop and nothing on Conflict/Error.

Adds Orchestrator.apply_rebase_with_anchor that combines apply_rebase_result
with a fold over the anchor events into a single atomic update. Each
Anchor_recorded calls Patch_agent.record_anchor (pushes onto
Anchor_history and mirrors into the legacy view fields);
Anchor_capture_failed events are ignored.

Runner Rebase path now calls apply_rebase_with_anchor and drops the
bespoke post_rebase_sha capture + the separate set_branch_rebased_onto_sha
update. The orchestrator transition and the anchor recording happen in a
single update_orchestrator_returning call.

Net result for an Ok rebase: branch_rebased_onto and
branch_rebased_onto_sha are populated identically to before, AND a new
entry is pushed onto anchor_history. For a Noop rebase: same behavior,
where previously the SHA was NOT captured — closing one of the three
blind spots.

for_merge_conflict is still the old 3-op plan; step 6 wires it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Worktree.S signature changes:

  - rebase_onto loses ?prev_base_sha:string option; gains required
    ~upstream:string. The caller (the executor) is now responsible for
    computing the right upstream from anchor history; W.rebase_onto's job
    is just to invoke git with what it's given. find_old_base's cherry-
    pick / patch-id detection stays as defense-in-depth: it still runs
    first; the supplied upstream is used only on its fallback path.
  - New is_ancestor : path -> ancestor -> descendant -> bool via
    [git merge-base --is-ancestor]. Returns false on any error.

Worktree_plan_executor.Rebase_onto handler now:

  1. Reads HEAD sha via W.read_branch_sha ~ref_name:"HEAD".
  2. Builds a Rebase_decision.input from the agent's anchor_history,
     base_branch, and the HEAD sha.
  3. Calls Rebase_decision.plan with W.is_ancestor as the ancestor_oracle.
  4. Passes Onto.upstream (or Plain.target as a fallback) to W.rebase_onto.

Deletes Rebase_decision.upstream and its RD-1..5 property tests.

For agents whose anchor_history is empty, plan returns Plain { target =
base_branch_name; reason = No_anchor }, the executor passes upstream =
target, and W.rebase_onto picks the 2-arg form. Bit-identical to today's
None-prev_base_sha path.

Test surface updates:

  - test_rebase_onto.ml: every direct W.rebase_onto call gains
    ~upstream:"<target_name>".
  - test_dependency_injection_functors.ml: Fake_worktree updated to the
    new signature; adds is_ancestor stub.
  - test_rebase_decision_properties.ml: deletes RD-1..5 (the shim
    tests); keeps the structured-plan suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
for_merge_conflict now includes Capture_anchor + Record_anchor_on_success
around its Rebase_onto, exactly like for_rebase (step 5.1).

Orchestrator.apply_conflict_rebase_with_anchor combines
apply_conflict_rebase_result with a fold over Worktree_plan.anchor_event
into a single atomic update. Same policy as apply_rebase_with_anchor:
Anchor_recorded calls Patch_agent.record_anchor; Anchor_capture_failed
is ignored (prior anchor preserved).

Runner Merge_conflict path now calls apply_conflict_rebase_with_anchor.
Net effect: a successful conflict rebase (Ok) now refreshes the agent's
anchor with the post-rebase origin/<base> SHA — closing the second of
the three blind spots from the design. A Noop conflict rebase also
refreshes the anchor; previously only branch_rebased_onto (the legacy
field) was set, never the SHA.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Runner Start path now executes Worktree_plan.for_start AFTER the worktree
exists but BEFORE the LLM session begins. The plan:

  Ensure_worktree -> Fetch_origin
    -> Capture_anchor {origin/<base_branch>, slot=0}
    -> Record_anchor_on_success {slot=0, base=base_branch}

emits an Anchor_recorded event with origin/<base_branch>'s current tip
SHA, which Orchestrator.apply_anchor_events folds into the agent via
Patch_agent.record_anchor.

Adds Orchestrator.apply_anchor_events for the events-only case (no
rebase result to combine).

Why this closes the production bug:

In the production state (ts-lowerability-effect-boundary-checker, p4/p5):
  - p4 was branched off patch-2 at Start.
  - branch_rebased_onto_sha was None at Start (recorded only on
    successful Rebase).
  - patch-2 squash-merged.
  - First rebase fired; with no anchor, Rebase_decision.plan returned
    Plain { reason = No_anchor }, the 2-arg form ran, and git replayed
    patch-2's pre-squash commits on top of main's post-squash version
    => "both added" conflict.

With this commit:
  - At Start, for_start captures origin/patch-2's tip SHA (51fce1265).
  - Anchor recorded in agent's anchor_history.
  - When patch-2 squash-merges and first rebase fires,
    Rebase_decision.plan returns Onto { upstream = 51fce1265; reason =
    Anchor_matches_head } because 51fce1265 is reachable from p4's HEAD.
  - git rebase --onto origin/main 51fce1265 HEAD replays just p4's own
    commit on top of main. No conflict.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 8 was originally specified as "invalidate the legacy anchor view
fields when refresh_base_branch retargets the base," but doing so breaks
the drift detector (PI-16) which relies on branch_rebased_onto reflecting
where the local branch was LAST REBASED, not where the orchestrator
currently WANTS the base to be.

The squash-merge / dep-retarget cases the original step 8 worried about
are already handled at the right layer: Rebase_decision.plan reads
anchor_history directly and uses an is_ancestor oracle at rebase time to
decide whether the recorded anchor is still safe for the new base. The
orchestrator transition has no oracle and shouldn't try to second-guess
the planner.

This commit adds a comment in refresh_base_branch explaining the
decision so a future "why doesn't this invalidate the anchor?" check
finds the answer in-line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test/test_rebase_state_machine.ml exercises realistic event sequences
against an in-memory commit DAG, asserting integration invariants
between Patch_agent.record_anchor, Rebase_decision.plan, and
Rebase_decision.anchor_after_result. Named scenarios:

  SM-1: dep merges WHILE a dependent's rebase is in flight; the
        recorded anchor remains coherent across the in-flight result.
  SM-2: production-bug scenario end-to-end. p4 branched off patch-2
        at Start records the patch-2 tip as its anchor; patch-2
        squash-merges; the next rebase planner returns Onto with the
        old patch-2 tip as upstream, never replaying patch-2's
        commits onto post-squash main.
  SM-3a: dep branch force-pushed; the recorded anchor is still
         reachable from the patch's HEAD, so plan still picks
         Anchor_matches_head.
  SM-3b: variant — the patch's HEAD itself was reset such that the
         anchor is no longer reachable; plan correctly returns
         Plain {Anchor_unreachable_from_head}.
  SM-4: 4-deep stack p1 <- p2 <- p3 <- p4; p1, p2, p3 squash-merge in
        sequence; p4's anchor stays valid throughout.
  SM-7: a Noop rebase result still refreshes the anchor — closes the
        old blind spot where the SHA was captured only on Ok.
  SM-8: a Conflict result preserves the prior anchor unchanged so a
        retry sees the same input.
  SM-no-anchor: legacy / fresh agent without any recorded anchor;
        plan returns Plain {No_anchor}, executor passes upstream=target,
        rebase falls back to the 2-arg form.

SM-5 (crash mid-plan), SM-6 (worktree deleted mid-rebase) require
orchestrator-level state interleaving and are deferred to a follow-up;
the pure-decision pipeline tested here is the load-bearing piece.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a "Rebase anchor lifecycle" section explaining:

  - The three recording moments (Start, Rebase Ok/Noop, Merge-conflict
    Ok/Noop) and the Worktree_plan ops that mediate them.
  - The Conflict/Error preservation policy.
  - How Rebase_decision.plan consumes anchor_history at rebase time
    using the is_ancestor oracle.
  - Why refresh_base_branch deliberately doesn't invalidate the anchor.
  - Pointers to the test suites that cover the pipeline end-to-end.

This is the last step of the stacked-pr-rebase-anchors workstream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 24, 2026

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

Project Deployment Actions Updated (UTC)
onton Ready Ready Preview, Comment May 24, 2026 6:00pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 24, 2026

📝 Walkthrough

Walkthrough

This PR implements rebase anchor history tracking across the patch orchestration pipeline. Anchors record upstream branch, resolved SHA, and remote-observation state. Patch agents now track a bounded (cap=8), newest-first, deduplicated history of anchors. Rebase planning uses this history plus an ancestor oracle to safely select upstreams. The executor captures origin refs into slots and emits anchor events on successful rebases. The orchestrator and runner atomically incorporate these events into agent state.

Changes

Rebase anchor history tracking

Layer / File(s) Summary
Anchor data types with validation
lib_core/anchor.{ml,mli}, lib_core/anchor_history.{ml,mli}
Anchor value type records branch, 40-char lowercase hex SHA, remote-observed flag with strict validation. Anchor_history is bounded (cap=8), newest-first, deduplicated list. Both include Yojson serialization with validation, safe defaults on parse failure.
Patch agent anchor tracking
lib_core/patch_agent.{ml,mli}, lib/persistence.ml
Patch_agent.t gains anchor_history field. Adds record_anchor (push to history, mirror to legacy fields), clear_anchor_for_base (invalidate legacy view), and anchor_history accessor. Persistence serializes/deserializes anchor_history with empty default for legacy snapshots.
Rebase decision planning logic
lib_core/rebase_decision.{ml,mli}
Replaces simple upstream string with structured plan function. Introduces reason (Anchor_matches_head / History_fallback / No_anchor / ...), plan (Onto {target, upstream, reason}
Worktree interface with upstream and oracle
lib/worktree.{ml,mli}
Updates rebase_onto signature: removes ?prev_base_sha, adds required ~upstream:string. Adds is_ancestor oracle (git merge-base --is-ancestor). Updates fallback logic to use caller-supplied upstream directly.
Worktree plan operations
lib_core/worktree_plan.{ml,mli}
Extends op type with Capture_anchor (ref_name, slot) and Record_anchor_on_success (slot, base). Adds anchor_event type (Anchor_recorded
Worktree plan executor
lib/worktree_plan_executor.{ml,mli}
Refactored to stateful loop carrying accumulated events, last_rebase outcome, and slot table. Capture_anchor reads branch SHA into slot. Rebase_onto computes upstream via Rebase_decision.plan. Record_anchor_on_success emits Anchor_recorded or Anchor_capture_failed based on slot/rebase outcome. Returns (rebase_result, path, anchor_event list).
Orchestrator anchor transitions
lib/orchestrator.{ml,mli}
Adds fold_anchor_events to apply anchor events to patch agent. Adds apply_rebase_with_anchor and apply_conflict_rebase_with_anchor: atomic wrappers composing rebase application + anchor folding. Adds apply_anchor_events for runner Start path. Documents base refresh preserving branch_rebased_onto for drift detection.
Runner anchor capture
lib/runner_fiber_impl.ml
Start action: executes for_start plan to capture start_anchor_events, applies via apply_anchor_events before session. Rebase action: executor returns 3-tuple (result, path, anchor_events); applies via apply_rebase_with_anchor. Conflict-Rebase: executor returns anchor_events; applies via apply_conflict_rebase_with_anchor.
Tests and documentation
test/test_anchor.ml, test/test_rebase_state_machine.ml, test/test_*.ml (updated), AGENTS.md
New test_anchor.ml: QCheck properties for Anchor validation, Anchor_history push/cap/dedup/yojson semantics. New test_rebase_state_machine.ml: in-memory DAG model with scenarios (SM-2..4, SM-7..8, SM-1) validating plan decisions and anchor preservation across production scenarios. Updated existing tests to wire anchor_history into Patch_agent.restore. AGENTS.md documents rebase anchor lifecycle with component/planning/recording details.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • flowglad/onton#290: Introduces the Worktree.S abstraction around rebase_onto; this PR adds ~upstream and is_ancestor to that interface.
  • flowglad/onton#225: Updates orchestrator merge-conflict rebase application; this PR threads anchor events through those same transitions atomically.
  • flowglad/onton#315: Modifies rebase planning in Rebase_decision; this PR replaces the older prev_base_sha logic with the new structured plan + anchor history.

🐰 With anchors now recorded through the rebase flow,
No more lost upstream context when patches grow!
History tracks each merge and force-push spree,
The planner picks safely—reachable, decree! 🌳

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Reliable stacked-PR rebase anchors' accurately summarizes the main feature addition: anchoring stacked-PR rebases to prevent the production bug where freshly-branched patches without anchors could conflict during first rebase.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch stacked-pr-rebase-anchors

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: 2 comments

Comment thread lib/worktree_plan_executor.ml Outdated
Comment thread lib_core/patch_agent.ml Outdated
@flowglad-review-service
Copy link
Copy Markdown

Review Summary

This is a well-structured behavior patch that correctly closes the squash-merge stacked-PR rebase trap. The pure planner (Rebase_decision.plan), anchor recording at Start/rebase/merge-conflict, executor slot mechanism, and persistence round-trip are all sound. The test suite (state-machine scenarios, property tests, round-trip tests) provides good coverage. I found one dead-code issue and one subtle semantic inconsistency in the executor.

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

Variant: convergence-v2

Candidates: 2 | Posted: 2 | Suppressed: 0


2 comments posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 52156 in / 4666 out · Cache: 255946 created / 1027641 read · Context: 5 items · Review mode: agentic · Turns: 15 · Tool calls: 22 · Forced submit: yes · Tool mix: read_file=18, search_codebase=3, find_references=1

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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib_core/anchor_history.ml`:
- Around line 24-27: of_yojson_opt currently returns Some (List.take xs cap)
after raw_of_yojson which only truncates by length and can leave duplicate
(base, sha) pairs; instead decode into xs and rebuild the history by feeding
entries through the module's push function to enforce invariants (uniqueness,
ordering, and capacity). Replace the direct List.take call with a fold over xs
that starts from the empty history and sequentially calls push (using push and
the module's empty/history constructor) so the resulting history is normalized
and respects cap; keep raw_of_yojson and cap as the sources of input and size
limit.

In `@test/test_worktree_plan.ml`:
- Around line 115-118: The test "for_start: includes Capture_anchor for
origin/<base> in slot 0" only checks ref_name via plan_has_capture_for(for_start
~base) but doesn't assert the slot; update the test to also assert the capture
slot equals 0. Modify the invocation to include an explicit slot check (e.g.,
call plan_has_capture_for with a slot argument like ~slot:0 if the helper
supports it, or add a separate assertion that inspects the returned capture's
slot from for_start ~base and compares it to 0) so that both the ref name
("origin/" ^ Branch.to_string base) and slot 0 are verified.
🪄 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: 82285ab5-9e35-4554-8c8a-abe8e5c77908

📥 Commits

Reviewing files that changed from the base of the PR and between b9f08ce and 38b5598.

📒 Files selected for processing (30)
  • AGENTS.md
  • lib/orchestrator.ml
  • lib/orchestrator.mli
  • lib/persistence.ml
  • lib/runner_fiber_impl.ml
  • lib/worktree.ml
  • lib/worktree.mli
  • lib/worktree_plan_executor.ml
  • lib/worktree_plan_executor.mli
  • lib_core/anchor.ml
  • lib_core/anchor.mli
  • lib_core/anchor_history.ml
  • lib_core/anchor_history.mli
  • lib_core/patch_agent.ml
  • lib_core/patch_agent.mli
  • lib_core/rebase_decision.ml
  • lib_core/rebase_decision.mli
  • lib_core/worktree_plan.ml
  • lib_core/worktree_plan.mli
  • test/dune
  • test/test_anchor.ml
  • test/test_dependency_injection_functors.ml
  • test/test_patch_agent.ml
  • test/test_patch_controller.ml
  • test/test_persistence_properties.ml
  • test/test_poll_log_properties.ml
  • test/test_rebase_decision_properties.ml
  • test/test_rebase_onto.ml
  • test/test_rebase_state_machine.ml
  • test/test_worktree_plan.ml

Comment thread lib_core/anchor_history.ml Outdated
Comment thread test/test_worktree_plan.ml Outdated
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_plan_executor.ml Outdated
@flowglad-review-service
Copy link
Copy Markdown

Review Summary

One must-fix correctness bug: in lib/worktree_plan_executor.ml, the Plain case of the upstream extraction passes plan.target (the logical branch name, e.g. "main") as ~upstream to W.rebase_onto, but W.rebase_onto compares upstream against its own rendered target string ("origin/main") to pick between the 2-arg and 3-arg rebase forms. The strings never match, so every no-anchor agent (the legacy fallback path) runs git rebase --onto origin/main main HEAD with an unresolvable "main" upstream instead of the intended git rebase origin/main. Fix: use Types.Branch.to_string target (the origin/-prefixed branch passed to W.rebase_onto) in the Plain case.

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

Variant: convergence-v2

Candidates: 1 | Posted: 1 | Suppressed: 0


1 comment posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 52137 in / 5713 out · Cache: 293179 created / 899670 read · Context: 5 items · Review mode: agentic · Turns: 14 · Tool calls: 15 · Tool mix: read_file=13, search_codebase=2

@flowglad-review-service
Copy link
Copy Markdown

Review Summary

After thorough review of the incremental push — covering Anchor, Anchor_history, Rebase_decision, Worktree_plan, Worktree_plan_executor, the runner wiring in runner_fiber_impl.ml, and all new test files — no new correctness bugs, security issues, or must-fix problems are introduced by this delta beyond the three outstanding comments from prior rounds.

Key verified properties:

  • Anchor_history.of_yojson_opt uses fold_right correctly; the round-trip preserves newest-first ordering.
  • for_start relies on the last_rebase = Worktree.Ok initial sentinel so Record_anchor_on_success fires correctly even without a prior Rebase_onto in the plan.
  • The agent snapshot is captured before execute runs; anchor history updates are applied only after execute returns via apply_anchor_events / apply_rebase_with_anchor, so no stale-read hazard.
  • The Plain case in the executor sets upstream = Types.Branch.to_string target, making String.equal upstream target true inside rebase_onto and triggering the correct 2-arg fallback — this is the fix for the previously-flagged must-fix (Turn 2 comment already live, not re-raised).
  • New test stanzas correctly link only onton_core (not onton) for test_anchor and test_rebase_state_machine, satisfying the property-test-pure-only-link convention.
Severity Count
Must-fix 0
Should-fix 0
Note 0

Variant: convergence-v2

Candidates: 0 | Posted: 0 | Suppressed: 0


0 comments posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 52170 in / 3931 out · Cache: 231315 created / 1053512 read · Context: 5 items · Review mode: agentic · Turns: 15 · Tool calls: 16 · Forced submit: yes · Tool mix: read_file=14, search_codebase=2

@subsetpark subsetpark merged commit 8cd4583 into main May 24, 2026
9 checks passed
@subsetpark subsetpark deleted the stacked-pr-rebase-anchors branch May 24, 2026 18:13
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