Conflict Resolution

What a conflict looks like inside Aura, and how to resolve it from the CLI or API.

Overview

A conflict is not a failure. It is the merge engine telling you the truth: two humans made decisions that cannot be reconciled without a human's judgment. Aura's job is to raise the smallest possible conflict, on the smallest possible node, with all three candidate versions in hand — so the judgment is cheap and the blast radius is tiny.

This page documents the anatomy of a conflict in Aura: its record structure, how it surfaces across CLI and API, and the full set of resolution verbs available. The interactive conflict picker covers the terminal UI in detail; this page is about the model underneath.

The best conflict is one a human resolves in under a minute. Shape the conflict to make that true.

How It Works

Conflicts are node-scoped, not file-scoped

When Aura's merger detects that ours and theirs cannot be combined at some node, it emits a Conflict record attached to that node — not to the whole file. A file with ten cleanly-merged changes and one real disagreement surfaces exactly one conflict entry. Every other change has already been applied.

The record contains enough to present, reason about, and resolve the conflict without re-parsing the file:

Conflict {
  id:         "c_8f21",
  file:       "src/auth.ts",
  node_path:  "module/class(AuthService)/method(login)",
  kind:       "function_declaration",
  reason:     "modify/modify",
  ancestor:   { text, ast_id, sha },
  ours:       { text, ast_id, sha, author, commit },
  theirs:     { text, ast_id, sha, author, commit },
  children:   [ sub-conflicts if any ],
  hints:      [ "disjoint statements? try semantic-union" ],
}

The reason field is authoritative

Aura classifies every conflict. The top-level reason is one of:

| Reason | Meaning | |---------------------|---------| | modify/modify | Both sides edited the same node differently. | | modify/delete | Ours modified a node theirs deleted (or vice versa). | | insert/insert | Both sides inserted different nodes at the same identity slot (e.g. two functions with the same name). | | rename/rename | Both sides renamed the same node to different names. | | type/type | Structural type mismatch (e.g. object vs array at the same JSON path). | | move/modify | One side moved the node, the other made a non-commutative edit at the old location. | | anchor/anchor | YAML anchor name collision with different targets. | | parse/parse | One side failed to parse; fell back to text merge with conflicts. |

The reason drives hints and strategy suggestions, and it is stable — external tooling can branch on it.

Conflicts compose into trees

A single conflict can contain sub-conflicts. Example: both sides rewrote login(), and Aura recursed into the function body to see if the edits were disjoint. They were partially disjoint — three statements merged cleanly, two conflict on the same if-branch. The top-level conflict on login() is marked with children: [...], and those children are leaf conflicts scoped to the if-branch. Resolving the children auto-resolves the parent; resolving the parent overrides the children.

Resolution verbs

Every conflict accepts the same verbs. The verb set is small on purpose:

| Verb | Result | |--------------|--------| | keep-ours | Apply ours's version at this node; discard theirs's edit. | | take-theirs| Apply theirs's version; discard ours's. | | keep-both | Apply both side by side when the node kind allows it (sequence items, overloads, multiple top-level declarations). Raises an error when it does not (scalar values, single fields). | | edit | Open an editor on the merged text with standard conflict markers and accept whatever is saved. | | revert | Fall back to the ancestor version — useful when both sides were wrong. | | defer | Leave the conflict in place, write the file with merge markers, and let the user handle it outside Aura. |

Each verb is an atomic operation. Resolutions are recorded in the merge session's audit log with the resolver's identity, timestamp, and the final node SHA.

The merge session

A merge is not a single command — it is a session. When you run aura merge <branch>, Aura:

  1. Computes all conflicts upfront and writes them to .aura/merge/<session-id>/conflicts.json.
  2. Applies all non-conflicting changes to the working tree.
  3. Marks the session in-progress in the repo metadata.
  4. Exits, or enters the interactive picker if --interactive is set.

Subsequent commands (aura conflicts list, aura resolve, aura merge continue, aura merge abort) operate against this session. The session commits only when all conflicts are resolved.

Examples

A: Listing conflicts from the CLI

$ aura conflicts list
session: merge/feature-login -> main (14 min ago)
status:  3 conflicts, 11 auto-merged

  c_8f21  src/auth.ts :: AuthService.login        modify/modify
  c_8f22  src/auth.ts :: AuthService.logout       modify/delete
  c_9a03  package.json :: /version               modify/modify

14 files auto-merged. Use `aura resolve <id>` to resolve individually,
or `aura merge --interactive` for the picker.

B: Inspecting a single conflict

$ aura conflicts show c_8f21
CONFLICT c_8f21
  file:   src/auth.ts
  node:   AuthService.login (method)
  reason: modify/modify

--- ancestor -------------------------------------
async login(user: string, pass: string) {
  return this.db.verify(user, pass);
}

--- ours (HEAD, Alice, 2h ago) --------------------
async login(user: string, pass: string) {
  const hashed = await bcrypt.hash(pass);
  return this.db.verify(user, hashed);
}

--- theirs (feature/login, Bob, 40m ago) ----------
async login(user: string, pass: string): Promise<User> {
  this.audit.track("login_attempt", user);
  return this.db.verify(user, pass);
}

hints:
  - both edits are disjoint statements inside login()
  - try: aura resolve c_8f21 --strategy semantic-union

C: Resolving non-interactively

$ aura resolve c_8f21 --strategy semantic-union
resolved c_8f21: semantic-union
  merged body contains: hashed-password guard + audit tracking

$ aura resolve c_8f22 keep-ours
resolved c_8f22: keep-ours (logout removed on theirs, retained on ours)

$ aura resolve c_9a03 take-theirs
resolved c_9a03: version=2.0.0

$ aura merge continue
all 3 conflicts resolved. applying merge commit...
merged: main <- feature/login (commit 1d4e2a3)

D: Programmatic resolution

Every verb is available over the Aura API, which is what the CLI, MCP tools, and UI share. Pseudocode against the Rust API:

let session = MergeSession::load(repo, session_id)?;
for conflict in session.conflicts() {
    match conflict.reason {
        Reason::ModifyDelete if conflict.ours_was_modified() =>
            session.resolve(conflict.id, Verb::KeepOurs),
        Reason::ModifyModify if conflict.disjoint_statements() =>
            session.resolve(conflict.id, Verb::Strategy("semantic-union")),
        _ => session.resolve(conflict.id, Verb::Defer),
    }?;
}
session.finalize()?;

The shape of this API is stable across the CLI, the MCP tool surface, and the library — the CLI is a thin wrapper.

E: Git-style markers for defer

When you defer a conflict, Aura writes a file with familiar markers so your editor or downstream tool can handle it:

<<<<<<< ours (AuthService.login)
async login(user: string, pass: string) {
  const hashed = await bcrypt.hash(pass);
  return this.db.verify(user, hashed);
}
||||||| ancestor
async login(user: string, pass: string) {
  return this.db.verify(user, pass);
}
=======
async login(user: string, pass: string): Promise<User> {
  this.audit.track("login_attempt", user);
  return this.db.verify(user, pass);
}
>>>>>>> theirs (feature/login)

The ancestor block (|||||||) is always included — this is the three-way "diff3" style. Aura tracks these markers: on the next aura merge continue, it re-parses and confirms you resolved them.

Edge Cases

Conflicts across files. Some operations (cross-file moves, package renames) surface as paired conflicts: one move in a.ts, one insert in b.ts. Resolving one auto-resolves the other when possible; otherwise both must be addressed.

Resolution that breaks the build. Aura's resolver does not run your test suite. It does run aura prove on the resolved file to confirm the semantic graph still connects. A resolution that deletes a function referenced elsewhere raises a secondary impact alert (see aura_live_impacts), surfaced before the merge commit lands.

Partial resolutions. You can resolve a subset of conflicts, run aura merge continue --allow-unresolved, and commit the partial state — but only to a branch, never to a protected branch. Unresolved conflicts are preserved in the session for later.

Abandoning a merge. aura merge abort rewinds the working tree to pre-merge state using the AST-level snapshots taken at session start. No stale markers, no half-applied changes.

Downgraded conflicts. A conflict initially flagged as modify/modify can downgrade if further inspection shows the edits are semantically equivalent after normalization (same logic, different variable names that both sides renamed consistently). Aura's normalizer runs once per conflict and is conservative — it never silently merges genuinely different logic — but it eliminates a meaningful class of false positives, particularly on projects with aggressive linting.

Audit trail. Every conflict record, every resolution verb, and every reveal action on masked content is written to .aura/merge/<session>/audit.jsonl as an append-only log. The log is committed along with the merge commit when the session finalizes, giving future reviewers complete visibility into how conflicts were decided and by whom.

Concurrent resolutions. On a team with Mothership Mode, two people can resolve different conflicts in the same session concurrently. The session log is append-only and linearized server-side; the CLI refuses to override a conflict that was resolved by someone else without --force.

See Also