Call Harmony inside the rebase replay loop for per-commit conflict learning
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 rebasecalls Harmony inside the replay loop. Deferred —--history rebaseruns 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:
- Per-commit conflicts during rebase are auto-resolved by Harmony when a matching pattern exists.
- Manually resolved conflicts during rebase are recorded by Harmony (human_verified=true, confidence=1.0) at the commit that introduced them.
- 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 inmuse/core/ormuse/cli/commands/rebase.py; confirm it is distinct from the squash path.RB_02— Callharmony_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— Callrecord_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'sours_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 rebasemerge 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
muse merge --history rebasecalls Harmony once per replayed commit, not once at the end.- HR_01–HR_04 all green.
- 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 mergeor--history squashHarmony paths (already correct). - New rebase features beyond the Harmony integration.
Implemented and shipped — commit
a7991bbcondev/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 standalonemuse rebasecommand (muse/core/rebase.py::replay_one+muse/cli/commands/rebase.py::_run_replay_loop).Confirmed with the ticket owner: this fix targets standalone
muse rebaseonly.muse merge --history rebaseremains 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.muse resolve+muse rebase --continue), the resolution was silently discarded:--continuewrote the new commit directly, bypassing therecord_resolutionscall thatmuse commitmakes for the equivalentmuse mergeconflict path (commit.py:772-800).MERGE_STATE.jsonwas also never cleared, left on disk indefinitely.Fix
replay_one()now callsharmony_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-keysharmony_resolvedto the file portion before merging into the manifest — the same muse#93 key-mismatch guard already inmerge.py._record_rebase_resolution()inmuse/cli/commands/rebase.py, called from--continueright after the paused commit is finalized — mirrorscommit.py's existing merge/squash pattern: builds ours/theirs manifests from MERGE_STATE's commit IDs, callsrecord_resolutions()withmanually_resolvedfrom MERGE_STATE, then unconditionally clearsMERGE_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:
_run_replay_loop()referenced an undefinedelapsedname when building conflict JSON —muse rebase --json <upstream>always crashed withNameErroron any conflict instead of reporting one._run_replay_loop()fell through to a barereturnon conflict with noSystemExit— the JSON body correctly said"exit_code": 1but the actual process exit code stayed0.state["remaining"]— but it had been read viastate["remaining"][0]and never popped, so the insert duplicated it. The next--continuewould 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):MERGE_STATE.jsonis cleared.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 rebaseremains 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.