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.
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.
| Altitude | Unit | When it fires | Conflict 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 |
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.
muse conflicts --json
[
{ "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
{
"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
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.
| Value | Behaviour | When 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.
muse merge feat/auth --explain --json
{
"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
{
"status": "ok",
"merge_type": "three_way",
"auto_resolved": ["src/auth/tokens.py::refresh_token"],
"conflicts": [],
"commit_id": "sha256:4e8b2f1a..."
}
Flags
| Flag | Default | Description |
|---|---|---|
--strategy | recursive | Merge strategy: recursive, overlay, snapshot, replay, ours, theirs |
--history | merge | History mode: merge, squash, rebase |
--on-conflict | escalate | Conflict fallback: escalate, ours, theirs |
--dry-run | off | Compute merge plan without touching working tree or MERGE_STATE |
--explain | off | Include per-path decision trace in JSON output |
--no-harmony-autoupdate | off | Skip Harmony auto-apply for this merge (still records conflicts on commit) |
--json | off | Structured JSON output |
Exit codes
| Exit code | Meaning |
|---|---|
0 | Merge complete (including fast-forward) |
1 | Merge paused — unresolved conflicts in working tree |
2 | Error — 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
| Flag | Default | Description |
|---|---|---|
--strategy | recursive | Same values as muse merge --strategy |
--history | merge_commit | merge_commit, squash, rebase (alias: merge → merge_commit) |
--on-conflict | escalate | Same values as muse merge --on-conflict |
--dry-run | off | Return merge plan without writing anything server-side |
--explain | off | Include per-path decision trace |
--json | off | Structured 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.
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
{
"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
explainlist 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 = trueandconfidence = 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.
| Task | Command |
|---|---|
| Merge a branch into current | muse merge <branch> --json |
| Dry-run merge plan | muse merge <branch> --dry-run --json |
| Merge with decision trace | muse 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 history | muse merge <branch> --history squash |
| Rebase history | muse merge <branch> --history rebase |
| Auto-take ours on conflict | muse merge <branch> --on-conflict ours |
| Find merge base | muse merge-base <a> <b> --json |
| List conflicts after partial merge | muse conflicts --json |
| Mark a path resolved (stages it) | muse resolve <path> --json |
| Mark all conflicts resolved | muse resolve --all --json |
| Commits not yet on target | muse rev-list <target>..<source> --json |
| Merge a proposal (server-side) | muse hub proposal merge <id> --json |
| Proposal merge dry-run | muse hub proposal merge <id> --dry-run --explain --json |
| Skip Harmony for one merge | muse merge <branch> --no-harmony-autoupdate |