gabriel / muse public
Closed #93 Bug
filed by gabriel human · 41 days ago

Data integrity: harmony auto-apply reports 'auto-resolved' but silently discards the recorded resolution's content

0 Anchors
— Blast radius
— Churn 30d
0 Proposals

Summary

muse merge's harmony auto-apply path (--harmony-autoupdate, on by default) reports success (✔ [harmony] auto-resolved: <path>) and increments the resolution's applied_count, but the actual merged content it writes is not the recorded resolution's outcome_blob — it silently falls back to whichever side happens to be "ours", discarding the explicit, human-verified decision that was supposedly just replayed.

This is a data-integrity bug, same tier as the zero-byte-file/code-domain issue from earlier this project (see musehub staging history) — the CLI actively reports a success message describing an action that did not actually happen.

Reproduction (confirmed twice, from a clean repo each time)

  1. Two branches diverge from a common base, each changing calc.py::add's body differently (return a + b + 1 vs return a + b + 100).

  2. Merge produces a real, correct, symbol-scoped conflict (CONFLICT (both modified): calc.py::add).

  3. Manually resolve to a third value, return a + b + 101, via muse resolve calc.py::add + muse commit.

  4. Confirm harmony recorded it correctly:

    $ muse harmony show <pattern-id> --json
    {
      "resolutions": [{
        "strategy": "manual", "confidence": 1.0, "human_verified": true,
        ...
      }]
    }
    

    The resolution's own outcome_blob, checked directly via muse cat-object, correctly contains return a + b + 101.

  5. Reproduce the identical conflict (same blob fingerprint) on a fresh pair of branches from the same original base.

  6. muse merge:

      ✔ [harmony] auto-resolved: calc.py::add
    Merge made by the three-way strategy.
    

    No conflict markers. Looks successful.

  7. But the actual resulting file (working tree and the committed snapshot, both checked — this is not a working-tree-refresh artifact) is:

    def add(a, b):
        return a + b + 1
    

    — i.e. exactly "ours" from step 5, byte for byte (the committed object ID matches the recorded pattern's own ours_id field exactly). Not +101. The recorded resolution was never actually applied.

  8. muse harmony show afterward confirms applied_count incremented to 1 on the resolution that was --- per the file contents --- never used.

Why this matters

This is the mechanism the whole "Harmony learns from you" pitch rests on (see docs/agent-guide.md's Harmony section, and the muse docs generally). If auto-apply silently keeps "ours" instead of the taught resolution:

  • Any agent or human trusting the "auto-resolved" message and moving on without inspecting the file has just committed the wrong merge result, with no error, no warning, and a false confidence increment (applied_count) implying the system worked correctly.
  • This defeats the entire point of recording resolutions in the first place --- the second (and every subsequent) occurrence of a taught conflict pattern produces silently incorrect output.

Hypothesis (not confirmed)

The auto-apply path likely finds the correct pattern/resolution (matching fingerprint), correctly decides not to surface a conflict, but the actual snapshot-write step defaults to the merge engine's ordinary "ours" branch of logic instead of writing read_object(repo_root, resolution.outcome_blob) into the merged manifest at that address. Needs an engineer tracing harmony_auto_apply (referenced from muse/cli/commands/merge.py) to confirm exactly where the resolution's content gets dropped.

Ask

  1. Root-cause exactly where harmony_auto_apply's selected resolution fails to make it into the merged manifest.
  2. Fix so the merged file's content actually matches outcome_blob, not "ours".
  3. Add a regression test: record a resolution whose outcome_blob differs from both "ours" and "theirs", reproduce the same conflict pattern again, and assert the merged file's content equals the resolution's outcome_blob --- not either original side. (The existing test suite apparently doesn't already cover this, or this would have been caught.)
  4. Audit whether the same gap exists in muse hub proposal merge (server-side merge), which per run_merge's own docstring shares this exact code path with the local CLI.

Priority

Higher than #91/#92 (found the same week, same project) --- those were either non-blocking classification quirks or a narrow false-conflict edge case. This one falsely reports success while discarding a human's explicit, recorded decision. Recommend treating as a data-integrity bug, not a backlog item, even though the season's episode 09 script doesn't have to wait for the fix (see the episode's own production notes for how it's being handled).

  • #91, #92 --- found the same week, different root causes, lower severity.

Source

Found by Claude Sonnet 5 while researching Episode 09 (Harmony), 2026-09-12.

◎ Activity1
●
gabriel opened this issue 41 days ago
○
gabriel 2 days ago

Root-caused, fixed, and shipped to source.

Root cause (confirmed via the exact repro in this ticket, reproduced live twice via muse-dev before touching any code): auto_apply() (muse/core/harmony/engine.py) correctly returns {symbol_path: outcome_blob} — for the code domain, symbol_path is a symbol address like "calc.py::add", not a plain file path. This part was already correct and already tested (TestAutoApplySymbolPaths in tests/test_harmony_integration.py).

The actual bug was one level up, in muse/cli/commands/merge.py:

merged_files = dict(result.merged["files"])   # keyed by plain file paths
merged_files.update(harmony_resolved)          # keyed by "file.py::symbol" -- MISMATCH

This inserted a bogus "calc.py::add" entry into a dict that's supposed to be keyed by plain file paths, while the real "calc.py" entry stayed at its stale pre-harmony value (which defaults to "ours"). That stale manifest is what later gets written to the working tree and committed — exactly matching every symptom in this ticket: the success message, the applied_count bump, and the silently-wrong content in both the working tree and the committed object.

Fix: one line — merged_files.update({p.split("::")[0]: oid for p, oid in harmony_resolved.items()}). Matches the identical addr.split("::")[0] convention already used by run_merge's own plugin-conflict path and by auto_apply itself elsewhere.

Ask #3 (regression test): tests/test_harmony_merge_commit_integrity.py reproduces your exact scenario end-to-end through the real CLI — two branches conflict on the same symbol, resolved manually to a third value distinct from both "ours" and "theirs", harmony confirmed to have recorded it, then the identical conflict pattern reproduced on a fresh branch pair. Asserts the merge reports "merged" and checks the actual committed object (via muse cat/muse cat-object, not just the working tree) matches the recorded resolution. Confirmed genuine RED against the unfixed code first. 797-test harmony+merge regression suite: zero regressions.

Ask #4 (musehub audit): not applicable. musehub/services/proposal_merge_strategies.py::execute_merge_strategy calls run_merge(...) without passing a plugin — so run_merge's symbol-level/harmony branch is never reached; musehub's merge strategies operate exclusively on plain file-path-keyed manifests. Harmony's auto_apply is never invoked anywhere in musehub (confirmed via muse code grep "auto_apply" — zero matches outside docs). No counterpart bug exists there today; worth re-checking if musehub ever starts passing a plugin for symbol-level server-side merges.

Not yet in a published release — same batching note as the other tickets this session.

Closing.

✓
closed this issue 2 days ago