Developer Docs Merge Engine
PHASE 05

Merge Engine

Muse merges at three altitudes — directory, file, and symbol — and resolves conflicts at the right level for each one. Where Git applies a single line-level diff to every file, Muse applies the merge algorithm that matches the content: recursive three-way merge for code, overlay for config, snapshot for binary domains, or replay for time-series data. The strategy, history mode, and conflict fallback are all independently configurable per merge. Harmony sits underneath, learning from every resolved conflict so future merges need less human intervention.

Merge ≠ conflict. Most Muse merges complete without conflicts. The three-level model means that changes to different symbols in the same file — the most common parallel-work pattern — are auto-merged without a conflict marker.

Overview

Git merges are line-level: two edits to different lines of the same function are "no conflict" even if they produce broken code together. Two edits to the same line always conflict even if they touch different semantic units.

Muse flips this: the unit of merging is the symbol — a function, class, variable, or domain-defined atom. Two agents editing different symbols in the same file never conflict. Two agents editing the same symbol always produce an inspectable three-way diff at the symbol level, not at an arbitrary line boundary.

The practical effect: teams working on the same file in parallel see far fewer spurious conflicts, and when a real conflict does occur, the diff is semantically meaningful — it shows exactly which function changed and why, not which line number happened to collide.

Three altitudes

The merge engine descends from coarse to fine, stopping at the level where the content is cleanly separable.

AltitudeUnitWhen it firesConflict address example
Directory A directory subtree One branch adds a directory the other deletes, or two branches diverge on the same directory with overlapping paths src/auth/
File A whole file Binary files, files the domain treats as atomic (e.g. .mid, lock files), or files where both branches made changes the domain cannot split into symbols src/config.json
Symbol A function, class, variable, or domain atom Code domain, identity domain, or any domain implementing diff_symbols(); two edits to the same symbol are a conflict; two edits to different symbols in the same file are auto-merged src/auth/tokens.py::validate_token
A conflict address like src/auth/tokens.py::validate_token uniquely identifies the exact symbol in conflict. muse conflicts --json returns the full list after any merge that does not complete cleanly.
bash list conflict addresses after a partial merge
muse conflicts --json
json output
[
  { "path": "src/auth/tokens.py::validate_token", "kind": "symbol" },
  { "path": "pyproject.toml",                     "kind": "file"   }
]

Engine model

Every merge is parameterised by two independent dimensions: the diff unit (how the engine splits content into comparable atoms) and the resolution policy (what the engine does when those atoms diverge). Named strategies are shorthand for specific combinations of these two dimensions.

Named strategy Diff unit Resolution policy Typical use
recursive Symbol (code domain) or line (text fallback) Three-way merge; conflict on divergent edits to the same atom Source code, structured text
overlay Key (for maps) or element (for sequences) Ours wins on key collision; additions from both sides kept Config files, package manifests
snapshot Whole file (atomic) Conflict if both branches modified the file; no sub-file merge attempted Binary blobs, MIDI, images
replay Domain-defined event (e.g. note, frame, row) Events replayed in causal order; duplicates suppressed Event-sourced domains, time-series
ours Whole file Always adopt ours; incoming branch changes discarded on conflict Generated files, lock files (use with a Harmony policy instead)
theirs Whole file Always adopt theirs; local branch changes discarded on conflict Vendor files that must match upstream exactly
ours and theirs silently discard work. Prefer a Harmony policy (muse harmony policy-add --action prefer-ours --path-pattern "*.lock") so the decision is recorded, auditable, and automatically applied on future merges. Use ours/theirs as a strategy only when you have confirmed that one side is entirely superseded.

Strategy reference

Pass --strategy to muse merge or muse hub proposal merge. The default is recursive.

# recursive — default; symbol-level three-way merge for code
muse merge feat/auth

# overlay — take additions from both sides; ours wins on key collision
muse merge feat/config --strategy overlay

# snapshot — atomic file merge; conflicts if both sides touched the file
muse merge feat/audio --strategy snapshot

# replay — causal event ordering; domain must implement replay semantics
muse merge feat/events --strategy replay

# ours — always take our version on conflict (⚠ discards incoming work)
muse merge feat/generated --strategy ours

# theirs — always take their version on conflict (⚠ discards local work)
muse merge upstream/vendor --strategy theirs

Dry-run before committing

Always switch to the target branch first. muse merge --dry-run returns the merge plan without touching the working tree or MERGE_STATE.

muse switch dev
muse merge feat/auth --dry-run --json
json dry-run output (fast-forward)
{
  "merge_type":         "fast_forward",
  "fast_forward":       true,
  "conflicts":          [],
  "files_added":        3,
  "files_modified":     7,
  "files_removed":      0,
  "commits_to_merge":   12
}

History modes

--history controls the shape of the commit graph after a successful merge. This is independent of the conflict resolution strategy.

Mode Graph shape Harmony effect When to use
merge (default) Two-parent merge commit; full feature branch history preserved All conflict resolutions from the branch are learned individually Long-lived features, shared work — preserve full attribution
squash Single commit on target branch; feature branch commits collapsed One resolution record per conflict path (the squash commit) Cleanup before merging experimental branches
rebase Feature branch commits replayed linearly on top of target Each replayed commit can generate independent Harmony records Linear history preference; CI pipelines that require no merge commits
muse merge feat/auth --history merge     # default
muse merge feat/auth --history squash    # collapse to one commit
muse merge feat/auth --history rebase    # replay commits linearly
Harmony learns from all three modes. Whether you use merge, squash, or rebase, Harmony records conflict resolutions from the resulting commit(s). The key difference is attribution granularity: merge preserves the full per-commit resolution history; squash collapses it to one record per conflict path.

The --on-conflict flag

--on-conflict is the conflict fallback policy — what the engine does when a conflict cannot be auto-resolved by Harmony and requires a decision. It does not replace Harmony; it fires only when Harmony has nothing to offer.

ValueBehaviourWhen to use
escalate (default) Merge pauses at the conflict. Working tree contains conflict markers. muse conflicts --json lists what needs manual resolution. Always, unless you have a specific automated pipeline reason to use the others
ours Automatically adopt our version for all unresolved conflicts. Merge completes without pausing. Automated pipelines where incoming changes must never override ours (e.g. merging back from a deploy branch)
theirs Automatically adopt their version for all unresolved conflicts. Merge completes without pausing. Automated pipelines where incoming changes always take precedence (e.g. syncing a vendor mirror)
# default — pause for human inspection
muse merge feat/auth

# CI pipeline — take ours for unresolved conflicts, never block
muse merge deploy/rc12 --on-conflict ours

# vendor sync — take theirs for everything unresolved
muse merge upstream/vendor --strategy snapshot --on-conflict theirs
--on-conflict ours and --on-conflict theirs silently discard work. The defaults (escalate) are safe — they pause the merge and let you inspect. Only use the non-default values in fully automated pipelines where you have already audited what will be discarded.

The --explain flag

muse merge --explain returns a per-path decision trace showing exactly why each file was merged the way it was — which strategy fired, which Harmony tier (if any) resolved it automatically, and what the outcome was.

bash merge with full decision trace
muse merge feat/auth --explain --json
json output — per-path decision trace
{
  "status":  "ok",
  "explain": [
    {
      "path":        "src/auth/tokens.py::validate_token",
      "altitude":    "symbol",
      "outcome":     "auto_merged",
      "strategy":    "recursive",
      "harmony_tier": null,
      "note":        "changes were in different symbol regions"
    },
    {
      "path":        "src/auth/tokens.py::refresh_token",
      "altitude":    "symbol",
      "outcome":     "harmony_applied",
      "strategy":    "recursive",
      "harmony_tier": 2,
      "resolution_id": "7c1a9e3f4d82...",
      "confidence":  1.0,
      "note":        "replayed human-verified resolution from 2026-05-14"
    },
    {
      "path":        "pyproject.toml",
      "altitude":    "file",
      "outcome":     "conflict",
      "strategy":    "overlay",
      "harmony_tier": null,
      "note":        "both branches modified 'version' key"
    }
  ]
}

--explain is safe with --dry-run — the full decision trace is computed without modifying the working tree:

muse merge feat/auth --dry-run --explain --json

muse merge — local merge

muse merge <branch> merges the named branch into the current branch. The current branch must be the target — switch to it before merging.

# always switch to the target branch first
muse switch dev
muse merge feat/auth --json
json clean merge output
{
  "status":         "ok",
  "merge_type":     "three_way",
  "auto_resolved":  ["src/auth/tokens.py::refresh_token"],
  "conflicts":      [],
  "commit_id":      "sha256:4e8b2f1a..."
}

Flags

FlagDefaultDescription
--strategyrecursiveMerge strategy: recursive, overlay, snapshot, replay, ours, theirs
--historymergeHistory mode: merge, squash, rebase
--on-conflictescalateConflict fallback: escalate, ours, theirs
--dry-runoffCompute merge plan without touching working tree or MERGE_STATE
--explainoffInclude per-path decision trace in JSON output
--no-harmony-autoupdateoffSkip Harmony auto-apply for this merge (still records conflicts on commit)
--jsonoffStructured JSON output

Exit codes

Exit codeMeaning
0Merge complete (including fast-forward)
1Merge paused — unresolved conflicts in working tree
2Error — invalid arguments, bad branch reference, or cannot merge a branch into itself

Resolving conflicts

When muse merge exits 1, the working tree contains conflict markers. The conflict address format (file.py::Symbol for symbol-level, file.py for file-level) is used in both muse conflicts --json and muse resolve.

# 1. List what needs resolving
muse conflicts --json

# 2. Read the conflicted file — understand all three sections:
#    <<<<<<< ours [modified]     ← what YOUR branch has
#    ||||||| base                 ← common ancestor
#    ======= theirs [modified]    ← what the INCOMING branch has
#    >>>>>>> end conflict

# 3. Edit the file to the correct merged result incorporating both sides

# 4. Mark resolved — stages automatically
muse resolve src/auth/tokens.py::validate_token --json
# or resolve all at once:
muse resolve --all --json

# 5. Commit — Harmony records the resolution (human_verified=true, confidence=1.0)
muse commit -m "merge: resolve validate_token conflict" \
  --agent-id claude-code --model-id claude-sonnet-4-6 --sign

Proposal merge

muse hub proposal merge merges a proposal's source branch into its target branch on the server. It accepts the same strategy, history, and on-conflict flags as local muse merge. The surface is intentionally identical so that an automated CI pipeline and a human reviewing a proposal see the same abstraction.

# merge proposal af54753d with squash history and overlay strategy
muse -C ~/ecosystem/musehub hub proposal merge af54753d \
  --strategy recursive \
  --history squash \
  --json | jq '.status'

# check what the merge would do before committing
muse -C ~/ecosystem/musehub hub proposal merge af54753d \
  --dry-run --explain --json

Strategy × history matrix — pick one of each

--history merge --history squash --history rebase
--strategy recursive Symbol-level three-way, full history Symbol-level three-way, collapsed history Symbol-level three-way, linear history
--strategy overlay Key-level merge, full history Key-level merge, collapsed Key-level merge, linear
--strategy snapshot Atomic file, full history Atomic file, collapsed Atomic file, linear

Proposal merge flags

FlagDefaultDescription
--strategyrecursiveSame values as muse merge --strategy
--historymerge_commitmerge_commit, squash, rebase (alias: mergemerge_commit)
--on-conflictescalateSame values as muse merge --on-conflict
--dry-runoffReturn merge plan without writing anything server-side
--explainoffInclude per-path decision trace
--jsonoffStructured JSON output

Safe merge protocol

After experiencing 8 accidental reverts from under-inspected merges, this workspace adopted a four-step pre-merge checklist. Follow it every time — it takes less than a minute and prevents hours of recovery work.

Create backup branches before every non-trivial merge. muse branch feat/my-thing-backup creates a pointer at the current feature branch HEAD without switching. If the merge goes wrong, the backup preserves the work.

Step 1 — Topology check

Find the merge base. A fast-forward topology means no conflicts are possible.

muse merge-base dev feat/my-thing --json
json output
{
  "merge_base": "sha256:abc123...",
  "commit_a":   "sha256:abc123...",   // dev tip
  "commit_b":   "sha256:def456..."    // feat/my-thing tip
}

If merge_base == commit_a (dev tip), the merge is a pure fast-forward — all feature branch commits are linear descendants of dev. No conflict is possible; skip Step 3 if you're confident.

Step 2 — Inspect unique commits

List the commits that will be merged, then spot-check the diff for surprising changes.

# commits on feat/my-thing not yet on dev
muse rev-list dev..feat/my-thing --json | jq '.commits[] | {message, committed_at}'

# overall diff summary
muse diff dev feat/my-thing --json | jq 'keys'

Step 3 — Dry-run merge

Switch to the target branch first, then dry-run. Running dry-run while still on the feature branch will error ("Cannot merge a branch into itself").

muse switch dev                                              # ← must switch first
muse merge feat/my-thing --dry-run --explain --json | jq

Verify:

  • "conflicts": [] — no conflicts expected
  • "fast_forward": true — pure FF if applicable
  • The explain list accounts for all files you expect to change

Step 4 — Merge, test, push

# merge
muse merge feat/my-thing --json

# run relevant tests
muse code test --json

# push (use --force-with-lease if remote ref needs advancing after FF merge)
muse push local dev
# or if rejected as diverged after a fast-forward:
muse push local dev --force-with-lease

# promote dev → main when ready
muse switch main
muse merge dev --dry-run --json
muse merge dev --json
muse push local main
muse switch dev
--force-with-lease vs --force — after a fast-forward merge locally, the remote ref may not have been updated (common after a previous push was rejected). --force-with-lease is safe: it advances the ref only if the remote tip matches your last fetch. --force skips that check. Always prefer --force-with-lease.

Harmony integration

Harmony is the conflict-resolution memory layer that sits beneath every merge. It fires automatically — you never call it directly during a merge. The relationship is:

  • Merge engine detects which paths conflict and at which altitude.
  • Harmony checks whether a prior resolution exists for each conflicting path, and if so, applies it automatically (Tier 1–3). No human intervention needed.
  • For paths Harmony cannot resolve (Tier 4), the merge pauses and conflict markers appear in the working tree.
  • When you manually resolve and commit, Harmony records the outcome with human_verified = true and confidence = 1.0. The same conflict auto-resolves on the next merge.
# merge — Harmony auto-applies anything it knows
muse merge feat/auth --json
# ✔ [harmony] auto-resolved: src/auth/tokens.py::refresh_token  (Tier 2)
# CONFLICT: pyproject.toml                                       (Tier 4 — new)

# resolve manually, commit — Harmony learns
muse resolve pyproject.toml --json
muse commit -m "merge: resolve pyproject.toml version conflict" \
  --agent-id claude-code --model-id claude-sonnet-4-6 --sign
# ✅ harmony: recorded resolution for 'pyproject.toml'

# next merge — auto-resolved
muse merge feat/other --json
# ✔ [harmony] auto-resolved: pyproject.toml  (Tier 2)

For the full Harmony reference — confidence thresholds, policies, semantic fingerprinting, audit log, and escalation — see Phase 05: Harmony.

CLI reference

All commands accept --json.

TaskCommand
Merge a branch into currentmuse merge <branch> --json
Dry-run merge planmuse merge <branch> --dry-run --json
Merge with decision tracemuse merge <branch> --explain --json
Three-way strategy (default)muse merge <branch> --strategy recursive
Overlay strategy (config)muse merge <branch> --strategy overlay
Snapshot strategy (binary)muse merge <branch> --strategy snapshot
Squash historymuse merge <branch> --history squash
Rebase historymuse merge <branch> --history rebase
Auto-take ours on conflictmuse merge <branch> --on-conflict ours
Find merge basemuse merge-base <a> <b> --json
List conflicts after partial mergemuse conflicts --json
Mark a path resolved (stages it)muse resolve <path> --json
Mark all conflicts resolvedmuse resolve --all --json
Commits not yet on targetmuse rev-list <target>..<source> --json
Merge a proposal (server-side)muse hub proposal merge <id> --json
Proposal merge dry-runmuse hub proposal merge <id> --dry-run --explain --json
Skip Harmony for one mergemuse merge <branch> --no-harmony-autoupdate