Merge Strategies
Four strategies, clear tradeoffs, and when each is the right tool.
Overview
The merge strategy is the top-level knob that tells Aura how aggressive to be when combining two sides. Most teams run with the default — semantic — and only reach for another strategy in specific situations: release branches that must take one side wholesale, monorepo imports from a vendor branch, or multi-author rewrites where every non-conflicting statement should merge.
Aura ships four strategies: semantic (default), prefer-ours, prefer-theirs, and semantic-union. Each is a deterministic function of (ancestor, ours, theirs) with clearly-specified conflict behavior. Each can be set globally, per-merge, per-file-glob, or per-conflict. And each honors the underlying file-type merger — strategies are not a replacement for AST merge or JSON deep merge, they are a policy on top of them.
A strategy is a promise about how conflicts are decided, not a license to ignore them. Even
prefer-ourspreserves full audit trail and calls attention to what was dropped.
How It Works
The strategy controls three things
- Classification of ties. When both sides modified the same node and the payloads are structurally similar but not identical, how strict is the equivalence test?
- Default resolution on conflict. When Aura raises a conflict, does it stop for human input (the default), or does it auto-resolve using the strategy's policy?
- Statement-level recursion. For
semantic-union, Aura recurses one level deeper than usual to merge disjoint statements inside the same function.
Strategy table
| Strategy | On modify/modify ties | On true conflict | Recurses into bodies | Typical use |
|-------------------|------------------------|----------------------|----------------------|-------------|
| semantic | Raise if payloads differ | Stop, ask human | Yes (one level) | Default for every merge |
| prefer-ours | Keep ours silently | Keep ours silently | Yes | Cherry-pick into a locked release branch |
| prefer-theirs | Take theirs silently | Take theirs silently | Yes | Re-import from an authoritative upstream |
| semantic-union | Attempt statement-level merge first | Raise only if sub-nodes truly conflict | Yes (deep) | Long-lived feature branches with many disjoint edits |
Strategy is a stack
Strategy selection walks a stack of sources, first-match wins:
--strategyflag onaura merge(highest precedence).- Per-file rule in
.aura/merge.json({ "strategies": { "src/legacy/**": "prefer-ours" } }). - Per-conflict override inside the interactive picker.
- Repo-wide default in
.aura/config.json. - Built-in default:
semantic.
Strategy decisions are recorded in the merge audit log with the source that provided them, so it is always clear why a given conflict auto-resolved.
Guardrails that apply regardless of strategy
Even under prefer-ours or prefer-theirs, Aura refuses to:
- Discard a function that
aura_provehad connected to a protected goal. - Delete a file flagged as load-bearing by the session's impact analysis without an explicit
--force. - Override an intent log that is pinned (
aura log-intent --pin) by a teammate on the opposite side.
These guardrails exist because silent-preference strategies are precisely where accidents are expensive. The strategy decides the normal cases; the guardrails catch the extraordinary ones.
Examples
A: semantic — the default
Ancestor, ours, theirs as in the AST merge walkthrough. semantic merges anything disjoint, raises conflicts on real disagreements, and leaves the decision to a human.
$ aura merge feature/payments
3 conflicts, 11 auto-merged
This is what you want 95% of the time.
B: prefer-ours for a hotfix branch
You are maintaining a release branch. A teammate's feature branch has drifted in directions you cannot accept for the release. You want to cherry-pick only the non-conflicting bits; every true conflict should keep the release branch's version.
$ aura merge feature/payments --strategy prefer-ours
0 conflicts raised. auto-resolved:
src/billing.ts :: calculateFee -> kept ours (theirs changed formula)
src/billing.ts :: postInvoice -> kept ours (theirs added new arg)
package.json :: /version -> kept ours
14 auto-merged.
The audit log records exactly what was dropped from theirs:
$ aura merge audit
session: merge/feature-payments -> release/1.4 (2m ago)
strategy: prefer-ours
3 resolutions auto-applied:
c_8f21 keep-ours "calculateFee body from theirs discarded"
c_8f22 keep-ours "postInvoice signature change from theirs discarded"
c_9a03 keep-ours "version bump from theirs discarded"
You can later review the dropped changes with aura conflicts show <id> --reveal-dropped if you want to rescue any of them in a follow-up PR.
C: prefer-theirs for a vendor import
A monorepo imports a vendored library by merging its upstream branch. You never want local edits to override upstream; any conflict means the local change is obsolete.
$ aura merge vendor/upstream --strategy prefer-theirs --scope "vendor/**"
8 auto-resolved (local edits dropped)
22 files auto-merged
The --scope flag confines the strategy to the vendored directory. Conflicts outside that path still fall under the repo's default semantic strategy.
D: semantic-union for a long-lived feature branch
Two developers have been working on the same authentication module for two weeks. Almost every function has edits on both sides, but the edits are mostly disjoint statements — one added logging, the other added validation. Default semantic would flag every function as modify/modify and require resolution.
$ aura merge feature/auth-refactor --strategy semantic-union
2 conflicts raised, 47 auto-merged (of which 31 statement-level unions)
Aura recursed into each function, diffed statement-by-statement, and produced a union of both side's additions where they did not overlap. The two remaining conflicts are genuine: both sides rewrote the same if-branch.
Statement union example:
// ancestor
function charge(amount: number) {
log("charging");
stripe.create({ amount });
}
// ours
function charge(amount: number) {
if (amount <= 0) throw new Error("invalid"); // +1
log("charging");
stripe.create({ amount });
}
// theirs
function charge(amount: number) {
log("charging");
stripe.create({ amount });
metrics.increment("charge"); // +1
}
// semantic-union merged
function charge(amount: number) {
if (amount <= 0) throw new Error("invalid");
log("charging");
stripe.create({ amount });
metrics.increment("charge");
}
E: Per-file strategy configuration
{
"strategies": {
"default": "semantic",
"src/generated/**": "prefer-theirs",
"docs/**": "semantic-union",
"package-lock.json": "prefer-theirs",
"CHANGELOG.md": "semantic-union",
"config/production/**": "prefer-ours"
}
}
This encodes three policies:
- Generated code always takes upstream.
- Documentation and changelogs union-merge so every team member's edits survive.
- Production configuration never silently accepts an incoming edit.
Edge Cases
semantic-union on scalar conflicts. Unions apply only to container nodes (blocks, sequences, object maps). A conflict on a scalar (a version number, a string literal) falls through to the underlying modify/modify conflict regardless of strategy — there is no meaningful union of 5 and 10.
Strategy and file-type mergers. The strategy is orthogonal to the file-type merger. prefer-ours over a YAML file uses YAML merge and applies prefer-ours only at conflict points; non-conflicting YAML changes from theirs still apply. You are not discarding the entire file, just the disagreements.
Strategy changes mid-session. You cannot change the top-level strategy once a merge session has started. You can, however, override individual conflict resolutions from the picker, which is strictly safer: it targets only the conflicts you see.
prefer-* on insert/insert collisions. Both sides inserted a function with the same name but different bodies. prefer-ours keeps ours's version and discards theirs's — potentially losing an unrelated implementation. Aura flags insert/insert collisions as high-severity in the audit log under any silent-preference strategy.
Interaction with --dry-run. Every strategy supports --dry-run which prints the resolution plan without writing. Useful for inspecting what prefer-theirs will silently drop before committing.
Interaction with protected branches. Many teams restrict silent-preference strategies on protected branches. Aura supports this via .aura/config.json:
{
"protected_branches": {
"main": { "allowed_strategies": ["semantic"] },
"release/*": { "allowed_strategies": ["semantic", "prefer-ours"] }
}
}
Attempts to merge into a protected branch with a disallowed strategy fail before the session starts, with a diagnostic that names the constraint.
Strategy default and onboarding. New repos start with semantic. Switching a large repo from Git's three-way text merge to Aura's semantic strategy typically eliminates 70-95% of the conflicts the team previously hit, depending on language mix and team size. The remaining conflicts are nearly always real disagreements that Git was also surfacing, just with more noise.
Strategy visibility in diffs. Commits produced by silent-preference strategies carry a trailer indicating the strategy used and how many conflicts were auto-resolved:
Aura-Strategy: prefer-ours
Aura-Auto-Resolved: 3
Aura-Merge-Audit: .aura/merge/session-abc.audit.jsonl
Code hosts that parse trailers (GitHub, GitLab) can surface these on PRs so reviewers know a non-default strategy was used.
Audit expectations. Silent-preference strategies shift risk from "conflict not resolved" to "change silently dropped." Aura mitigates this with mandatory audit entries, but teams using prefer-ours or prefer-theirs should set up review workflows that inspect the strategy audit summary on every merge. The CLI emits a machine-readable summary (aura merge audit --json) suitable for PR bots.
Strategy composition. Strategies do not nest. A file matched by prefer-ours does not get semantic-union inside it; the whole file is under prefer-ours. This keeps the mental model simple. If you need semantic-union on a subset of a file, use the interactive picker and apply u per conflict.
Scripted overrides. Teams with custom resolution policies can supply a strategy-plugin binary that Aura consults per conflict. The plugin receives the conflict JSON on stdin and returns a verb on stdout. Plugins are recorded in the audit log by path and hash.