gabriel / muse public
fix patch fix/33-harmony-rebase-replay-loop #1 / 1
AI Agent gabriel · 3 hours ago · Oct 22, 2026 · Diff

fix: call Harmony inside muse rebase's per-commit replay loop (muse#33)

Scope confirmed with the user as standalone `muse rebase` only — the `muse merge --history rebase` flag is a pre-existing squash-equivalent stub (see merge.py's own comment: "rebase full commit-by-commit replay is Phase 4 scope") and is left untouched; it is not a genuine per-commit replay loop, so it is out of scope here and remains a known, documented gap to be addressed separately.

Root cause: `replay_one()` (muse/core/rebase.py), the real per-commit replay loop backing `muse rebase`, performed a three-way merge per commit but never consulted Harmony at all — a conflict always paused the rebase even when Harmony held a saved resolution for the identical pattern. And once a rebase conflict WAS resolved manually via `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 an equivalent `muse merge` conflict (muse/cli/commands/commit.py:772-800). This also left `MERGE_STATE.json` on disk forever, since nothing ever cleared it.

Fix: - `replay_one()` now calls `harmony_auto_apply()` with THIS commit's own ours/theirs manifests (not any squash snapshot) when the merge conflicts, before giving up. Re-keys `harmony_resolved` to the file portion before merging into the manifest (same muse#93 key-mismatch guard already in merge.py). - Added `_record_rebase_resolution()` in the CLI layer, called from `muse rebase --continue` right after the paused commit is finalized — mirrors commit.py's existing merge/squash pattern exactly: 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.

Three unrelated pre-existing bugs surfaced by writing a real end-to-end test against the actual CLI (rather than synthetic objects) and had to be fixed to make the scenario testable at all: - `_run_replay_loop()` referenced an undefined `elapsed` name when building conflict JSON output — always raised NameError, meaning `muse rebase --json <upstream>` crashed on any conflict instead of reporting one. - Both callers of `_run_replay_loop()` fell through to a bare `return` on conflict with no `SystemExit` — the JSON body correctly reported `"exit_code": 1` but the actual process exit code stayed 0. - The conflict branch in `_run_replay_loop()` re-inserted the commit ID at the front of `state["remaining"]`, duplicating it — it was read via `state["remaining"][0]` but never popped, so re-inserting it produced two copies. The next `--continue` would then replay the already-resolved commit a second time and report a spurious second conflict.

New test file tests/test_harmony_rebase_loop.py (HR_01-HR_04): a mid- replay conflict resolved manually is recorded by Harmony and clears MERGE_STATE.json; an identical conflict pattern on a fresh rebase is auto-resolved without pausing. Confirmed genuine RED by reverting the harmony wiring and re-running (both failures land exactly where the removed logic was supposed to act), then GREEN with the fix restored.

Regression: full harmony + rebase suites (755 tests), plus HA_02/HA_03/ HC_01-04 (tests/test_phase4_harmony_history.py, tests/test_phase7_merge_correctness.py) — all green, no regressions.

sha256:a7991bbc89b2af67cdd97580946aad7500cc737dd0c0d89088df831e198bdf84 sha
+18 ~5 symbols
3 changed · 1254 in snapshot files
sha256:ab1060ef7f2988aa89a05b8089fbfc59d7d2cc75ec3717111a091c19b22264a1 snapshot
+18
symbols added
~5
symbols modified
3
files changed
1254
files in snapshot
0
dead code introduced
Semantic Changes 23 symbols
~ tests/test_harmony_rebase_loop.py .py 12 symbols added
+ _branch_from function function _branch_from L52–58
+ _commit_calc function function _commit_calc L44–49
+ _run function function _run L35–41
+ CliRunner import import CliRunner L30–30
+ InvokeResult import import InvokeResult L30–30
+ annotations import import annotations L24–24
+ json import import json L26–26
+ os import import os L27–27
+ pathlib import import pathlib L28–28
+ runner variable variable runner L32–32
+ test_rebase_auto_resolves_identical_conflict_via_harmony function function test_rebase_auto_resolves_identical_conflict_via_harmony L112–170
+ test_rebase_conflict_resolution_is_recorded_and_merge_state_cleared function function test_rebase_conflict_resolution_is_recorded_and_merge_state_cleared L61–109
~ muse/cli/commands/rebase.py .py 4 symbols added, 4 symbols modified
+ _record_rebase_resolution function function _record_rebase_resolution L182–230
+ clear_merge_state import import clear_merge_state L50–50
+ harmony_record_resolutions import import harmony_record_resolutions L49–49
+ read_merge_state import import read_merge_state L50–50
~ run
~ muse/core/rebase.py .py 2 symbols added, 1 symbol modified
+ dataclasses class import dataclasses L47–47
+ harmony_auto_apply import import harmony_auto_apply L71–71
Files Changed
+1 ~2
1254 in snapshot
← Older Oldest on fix/33-harmony-rebase-replay-loop
All commits
Newer → Latest on fix/33-harmony-rebase-replay-loop

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:a7991bbc89b2af67cdd97580946aad7500cc737dd0c0d89088df831e198bdf84 --body "your comment"