Bug: overlay strategy reports phantom conflicts when both sides have identical content
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)
Fixed and fully tested. PHANTOM_01–PHANTOM_05 all pass in
tests/test_phantom_conflicts.pyin the muse repo. The guard (ours_object_id == theirs_object_id → skip) was added to bothoverlayandstate_mergestrategies, and SM_22–SM_24 intest_phase3_strategy_matrix.pyand PG_06–PG_08 intest_phase5_phantom_guard.pyextend coverage to the remaining strategies and the musehub-side merge paths.