gabriel / musehub public
Closed #85 Bug
filed by gabriel human · 3 days ago

Bug: overlay strategy reports phantom conflicts when both sides have identical content

0 Anchors
Blast radius
Churn 30d
0 Proposals

Bug

`muse merge --strategy overlay` (and by extension `muse hub proposal merge --strategy overlay`) reports symbol-level conflicts even when the object ID for a file is identical on both sides. The working tree is already correct, `muse diff` shows nothing, and there are no inline conflict markers — but `muse conflicts --json` lists symbols as unresolved.

Repro

```bash

Branch A and Branch B both modify ui_issues.py independently.

wire_fetch.py is NOT touched by either branch — it's unchanged on both sides.

muse hub proposal merge <proposal-id> --strategy overlay --history merge

Pull result into local dev:

muse pull staging dev

Result:

❌ Merge conflict in 5 file(s):

CONFLICT (both modified): musehub/services/musehub_wire_fetch.py::_CommitDeltaMap

CONFLICT (both modified): musehub/services/musehub_wire_fetch.py::_walk_commit_delta

...

```

Root cause

The merge engine computes "which symbols were touched in each branch's history" and flags them as conflicted. But it does not short-circuit when `ours_object_id == theirs_object_id`. When both branches independently arrive at the same file content — or when a file was never touched by either branch — the object IDs match exactly and there is nothing to resolve. The conflict report is a false positive.

Confirmed: in the session that surfaced this bug, both ours and theirs had:

``` musehub/services/musehub_wire_fetch.py sha256:b7341345f5251… (identical) musehub/services/musehub_wire_shared.py sha256:92f0120909bb4… (identical) ```

Why this is dangerous

When an agent hits phantom conflicts it is forced to `muse resolve --all` and `muse commit --allow-empty`. The `--allow-empty` merge commit uses the ours-side snapshot, silently discarding any theirs-side file changes that happened to land in the same merge operation. This is the mechanism by which agents lose work during merges — phantom conflicts force `--allow-empty`, and `--allow-empty` picks the wrong snapshot.

Fix

In the conflict detection path for `overlay` strategy (and all strategies), add a guard:

```python if ours_object_id == theirs_object_id: # Content is identical — no conflict possible. Skip. continue ```

This must be applied before any symbol-level conflict is recorded in `MERGE_STATE`. If the file-level object IDs match, no symbol within that file can have a real conflict.

Test plan (TDD — write tests first)

All tests must be red before the fix and green after.

PHANTOM_01 — overlay: identical file, no conflict reported

``` Setup: repo with branches A and B; both branches touch file_x.py; file_y.py is untouched by either branch (same content as base). Merge: muse merge B --strategy overlay Assert: muse conflicts --json → [] (empty) file_y.py is not listed in conflict_paths ```

PHANTOM_02 — overlay: both sides converge on same content, no conflict

``` Setup: base has file_x.py at version V1. Branch A modifies file_x.py to V2. Branch B independently modifies file_x.py to V2 (same result). Merge: muse merge B --strategy overlay Assert: muse conflicts --json → [] file_x.py content == V2 ```

PHANTOM_03 — state_merge: same guard applies

Repeat PHANTOM_01 and PHANTOM_02 with `--strategy state_merge`. Identical content must never produce a conflict.

PHANTOM_04 — real conflict still detected

``` Setup: base has file_x.py at V1. Branch A modifies to V2. Branch B modifies to V3 (different). Merge: muse merge B --strategy state_merge Assert: muse conflicts --json lists file_x.py (real conflict, correctly detected) ```

PHANTOM_05 — --allow-empty is never needed after a clean merge

``` Setup: same as PHANTOM_01. Merge: muse merge B --strategy overlay Assert: muse conflicts --json → [] muse commit (no --allow-empty needed) succeeds merge commit snapshot contains content from BOTH branches (not just ours) ```

PHANTOM_05 is the load-bearing regression test. It must verify that the merge commit snapshot is correct — not just that conflicts are absent.

Acceptance criteria

  • PHANTOM_01 through PHANTOM_05 pass
  • `muse merge --strategy overlay` never reports a conflict when `ours_object_id == theirs_object_id`
  • `muse merge --strategy state_merge` same
  • `--allow-empty` is never required to complete a conflict-free merge
  • Merge commit snapshot after a clean merge reflects both sides' changes (regression guard against the snapshot-picks-ours bug)
  • Existing merge conflict tests unaffected (real conflicts still detected)
Activity1
gabriel opened this issue 3 days ago
gabriel 2 days ago

Fixed and fully tested. PHANTOM_01–PHANTOM_05 all pass in tests/test_phantom_conflicts.py in the muse repo. The guard (ours_object_id == theirs_object_id → skip) was added to both overlay and state_merge strategies, and SM_22–SM_24 in test_phase3_strategy_matrix.py and PG_06–PG_08 in test_phase5_phantom_guard.py extend coverage to the remaining strategies and the musehub-side merge paths.

closed this issue 2 days ago