gabriel / muse public
Closed #33
filed by gabriel human · 134 days ago

Call Harmony inside the rebase replay loop for per-commit conflict learning

0 Anchors
— Blast radius
— Churn 30d
0 Proposals

Background

When muse merge --history rebase runs, the feature branch commits are replayed linearly on top of the target branch. Today, Harmony is called once at the end of the rebase — at commit time — which means it sees the final squash-equivalent result, not each individual replayed commit.

This was explicitly deferred in issue #86 (Phase 4):

muse rebase calls Harmony inside the replay loop. Deferred — --history rebase runs squash-equivalent in Phase 3; per-commit Harmony during replay not yet implemented.

The consequence: when a rebase produces a conflict mid-replay (e.g. commit 3 of 7 conflicts with the target), Harmony has no per-commit context to learn from. It cannot record a resolution keyed to the original commit IDs. Future rebases of similar branches won't benefit from any auto-resolution learned during the first rebase.

The correct model: Harmony must be called inside the rebase replay loop, once per replayed commit, at the moment each commit is applied. This mirrors how --history merge works — resolutions are recorded per-conflict, per-commit, with full provenance.


Goal

muse merge --history rebase calls Harmony inside the replay loop so that:

  1. Per-commit conflicts during rebase are auto-resolved by Harmony when a matching pattern exists.
  2. Manually resolved conflicts during rebase are recorded by Harmony (human_verified=true, confidence=1.0) at the commit that introduced them.
  3. A subsequent rebase of a similar branch auto-resolves the same per-commit conflicts without human intervention.

Phases

Phase 1 — Rebase engine Harmony integration

Deliverables:

  • RB_01 — Identify the rebase replay loop in muse/core/ or muse/cli/commands/rebase.py; confirm it is distinct from the squash path.
  • RB_02 — Call harmony_auto_apply(root, conflict_paths, …) inside the replay loop after each commit is applied and any conflicts are detected — not only at final commit time.
  • RB_03 — Call record_resolutions(root, …) at each replayed commit's resolution point so Harmony learns from per-commit context (not just the final squash-equivalent state).
  • RB_04 — Harmony recording during rebase uses the replayed commit's ours_id/theirs_id (not the squash snapshot IDs), preserving full per-commit provenance.

Phase 2 — Tests

One new test file: tests/test_harmony_rebase_loop.py.

Deliverables:

  • HR_01 — After a --history rebase merge that had a mid-replay conflict resolved manually, re-running the same rebase auto-resolves via Harmony. (Previously: HA_03 passed only via squash-equivalent path — this test must verify the per-commit Harmony path specifically.)
  • HR_02 — Harmony records the resolution keyed to the replayed commit's conflict context, not the final squash snapshot.
  • HR_03 — A rebase with multiple conflicting commits records independent Harmony entries for each conflict.
  • HR_04 — No regression to HA_02 (squash Harmony path) or HA_03 (existing rebase squash-equivalent path).

Acceptance criteria

  1. muse merge --history rebase calls Harmony once per replayed commit, not once at the end.
  2. HR_01–HR_04 all green.
  3. No regression to existing Harmony tests (HA_02, HA_03, HC_01–HC_04) or rebase tests.

Out of scope

  • Changes to Harmony's storage format or confidence-gating logic (Phase 7 of #86).
  • Changes to --history merge or --history squash Harmony paths (already correct).
  • New rebase features beyond the Harmony integration.
◎ Activity1
●
gabriel opened this issue 134 days ago
○
gabriel 2 days ago

Implemented and shipped — commit a7991bbc on dev/main (pushed to local/staging/production).

Scope confirmed before implementation

The ticket's acceptance criteria named muse merge --history rebase, but investigation showed that flag is a pre-existing squash-equivalent stub (muse/cli/commands/merge.py's own comment: "rebase full commit-by-commit replay is Phase 4 scope") — it performs a single collapsed merge, not a genuine per-commit replay loop. The only real per-commit replay loop in the codebase is the standalone muse rebase command (muse/core/rebase.py::replay_one + muse/cli/commands/rebase.py::_run_replay_loop).

Confirmed with the ticket owner: this fix targets standalone muse rebase only. muse merge --history rebase remains the documented squash-equivalent stub — a separate, pre-existing gap, not silently addressed here.

Root cause

  • replay_one() performed its per-commit three-way merge but never consulted Harmony at all — any conflict always paused the rebase, even when Harmony already held a saved resolution for the identical pattern.
  • Once a rebase conflict WAS resolved manually (muse resolve + muse rebase --continue), the resolution was silently discarded: --continue wrote the new commit directly, bypassing the record_resolutions call that muse commit makes for the equivalent muse merge conflict path (commit.py:772-800). MERGE_STATE.json was also never cleared, left on disk indefinitely.

Fix

  • replay_one() now calls harmony_auto_apply() using this commit's own ours/theirs manifests (not any squash snapshot) when its merge conflicts, before giving up (RB_02, RB_04). Re-keys harmony_resolved to the file portion before merging into the manifest — the same muse#93 key-mismatch guard already in merge.py.
  • Added _record_rebase_resolution() in muse/cli/commands/rebase.py, called from --continue right after the paused commit is finalized — mirrors commit.py's existing merge/squash pattern: builds ours/theirs manifests from MERGE_STATE's commit IDs, calls record_resolutions() with manually_resolved from MERGE_STATE, then unconditionally clears MERGE_STATE.json (RB_03).

Three unrelated pre-existing bugs found and fixed

Writing a real end-to-end test against the actual CLI (rather than synthetic objects) surfaced three latent bugs in the existing replay loop, all needed to make the scenario even testable:

  1. _run_replay_loop() referenced an undefined elapsed name when building conflict JSON — muse rebase --json <upstream> always crashed with NameError on any conflict instead of reporting one.
  2. Both callers of _run_replay_loop() fell through to a bare return on conflict with no SystemExit — the JSON body correctly said "exit_code": 1 but the actual process exit code stayed 0.
  3. The conflict branch re-inserted the commit ID at the front of state["remaining"] — but it had been read via state["remaining"][0] and never popped, so the insert duplicated it. The next --continue would replay the already-resolved commit a second time and report a spurious second conflict.

Tests

New file tests/test_harmony_rebase_loop.py (HR_01–HR_04):

  • A mid-replay conflict resolved manually is recorded by Harmony and MERGE_STATE.json is cleared.
  • An identical conflict pattern on a fresh rebase is auto-resolved by Harmony without pausing.

Confirmed genuine RED by reverting the harmony wiring and re-running (both failures landed exactly where the removed logic was supposed to act), then GREEN with the fix restored.

Regression: full harmony + rebase test files (763 tests) green, including HA_02/HA_03/HC_01–04 (tests/test_phase4_harmony_history.py, tests/test_phase7_merge_correctness.py) — no regressions.

Out of scope (flagged, not fixed)

muse merge --history rebase remains a squash-equivalent stub with no per-commit Harmony integration. Making it a true per-commit replay (closing issue #86 Phase 4) is a separate, larger piece of work and was explicitly excluded from this ticket's scope.

✓
closed this issue 2 days ago