---
name: mk:resolving-merge-conflicts
preamble-tier: 2
version: 1.0.0
description: |
  Resolve an in-progress git merge or rebase conflict end to end: read the
  conflict state, recover each side's original intent, resolve every hunk,
  run the project's checks, and finish the merge/rebase with a commit.
  Use when asked to "resolve merge conflicts", "fix this rebase", "finish the
  merge", or when `git status` shows conflicted (unmerged) paths.
  NOT for the full ship pipeline that merges the base branch first (see mk:ship);
  NOT for debugging a runtime bug or test failure unrelated to a conflict
  (see mk:investigate / mk:fix).
allowed-tools:
  - Bash
  - Read
  - Edit
  - Grep
  - Glob
keywords:
  - merge-conflict
  - rebase-conflict
  - git-merge
  - git-rebase
  - conflict-resolution
  - unmerged-paths
  - three-way-merge
when_to_use: Use when a git merge or rebase is in progress and has conflicted paths to resolve. NOT for the base-branch merge step of shipping (see mk:ship); NOT for non-conflict bugs (see mk:investigate).
user-invocable: true
owner: git
criticality: high
status: active
runtime: claude-code
---

# Resolving Merge Conflicts

> **Path convention:** Commands assume cwd is the repo root (`$CLAUDE_PROJECT_DIR`).

**Iron Laws**

- **Always resolve. Never `git merge --abort` / `git rebase --abort`** — finishing the
  operation is the goal. Abort only if the user explicitly asks.
- **Do not invent new behaviour.** Resolution combines existing intents; it is not a
  place to add features or refactor.

## When to Use

- `git status` shows `Unmerged paths` / "you have unmerged files"
- A `git merge`, `git rebase`, `git cherry-pick`, or `git pull` stopped on conflicts
- User says "resolve the conflicts", "fix the rebase", "finish the merge"

**Do NOT use when:** no operation is in progress (nothing to resolve), or the request is
to start a merge/ship from scratch (use `mk:ship`).

## Workflow Integration

**Phase: on-demand.** Most often fires inside Phase 5 (Ship) when `mk:ship` merges the
base branch and hits conflicts. Hands control back once the working tree is clean and
checks pass.

## Process

Copy this checklist and track progress:

```
- [ ] 1. See the current state (operation type + conflicted files)
- [ ] 2. Find the primary sources (why each side changed)
- [ ] 3. Resolve each hunk (preserve both intents)
- [ ] 4. Run the project's automated checks
- [ ] 5. Finish the merge/rebase
```

### 1. See the current state

Determine whether a **merge** or a **rebase** is in progress — the finish step differs.

```bash
git status                                   # human summary + hint line
git diff --name-only --diff-filter=U         # exact conflicted (unmerged) files
ls .git/MERGE_HEAD 2>/dev/null && echo MERGE # present => merge in progress
ls -d .git/rebase-merge .git/rebase-apply 2>/dev/null && echo REBASE
git log --oneline -5 --all --decorate        # orient on recent history
```

Read every conflicted file. Note each `<<<<<<< / ======= / >>>>>>>` hunk and which
labels mark each side (`HEAD`/`ours` vs the incoming branch/commit).

> **Rebase side-swap:** during a rebase `ours` is the branch being rebased ONTO and
> `theirs` is your commit being replayed — the opposite of a merge. Confirm before
> assuming which side is "yours".

### 2. Find the primary sources

For each conflict, understand *why* each change was made — do not guess from the diff alone.

```bash
git log --merge -p -- <file>                 # commits touching the conflict on both sides
git log -p <side> -- <file>                  # full history of one side's hunk
gh pr list --state merged --search <term>    # the PR that introduced the change
gh pr view <num> ; gh issue view <num>       # stated intent, linked tickets
```

Read commit messages, PR descriptions, and linked issues. The goal is the *original
intent* behind each side, not just the textual difference.

### 3. Resolve each hunk

For every hunk, in order of confidence:

1. **Preserve both intents** where they are compatible — combine the two changes so
   neither is lost. This is the default and most common outcome.
2. **Where incompatible**, pick the side matching the **stated goal of the merge/rebase**
   (e.g. "merge feature X into main" → keep X's behaviour) and note the trade-off so the
   discarded intent is visible to the user.
3. Remove **all** conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`). Verify none remain:
   `git grep -nE '^(<{7}|={7}|>{7})' -- <files>` returns nothing.
4. Stage each file as it is finished: `git add <file>`.

Never resolve by blindly taking one whole side (`-X ours`/`-X theirs`) unless the file's
two sides are genuinely one-or-the-other — that silently drops the other intent.

### 4. Run the project's automated checks

Discover the checks before running them (do not assume). Typical order: **typecheck →
tests → format.**

```bash
cat package.json        # scripts: typecheck / lint / test / build / format
ls Makefile justfile pyproject.toml Cargo.toml go.mod 2>/dev/null
```

Run what exists (for this kit: `npm run typecheck`, `npm test`, `npm run lint`). Fix
anything the merge broke — a resolution can be marker-free yet semantically wrong (e.g.
a function both sides renamed differently). Re-run until green.

### 5. Finish the merge/rebase

- **Merge:** stage everything, then commit. The prepared message is usually correct;
  keep it conventional and free of AI references.
  ```bash
  git add -A
  git commit --no-edit        # or write a conventional message if none is prepared
  ```
- **Rebase:** continue, and **loop** — a rebase pauses once per conflicting commit.
  ```bash
  git add -A
  git rebase --continue       # repeat steps 1-4 for each subsequent pause
  ```
  Repeat until `git status` reports the rebase is complete (no further stops).

Confirm a clean tree (`git status` → "nothing to commit, working tree clean") before
handing off.

## Output Format

```
## Merge Conflict Resolution: {merge|rebase} of {source} into {target}

**Conflicted files:** {n}
**Resolution summary:**
- {file}: {kept both | chose {side} because {merge goal} — dropped {discarded intent}}

**Checks:** typecheck {pass/fail} · tests {pass/fail} · format {pass/fail}
**Finished:** {commit sha | rebase complete}
```

## Failure Handling

| Failure | Recovery |
|---------|----------|
| Cannot tell which side's intent should win | Surface both intents + the merge's stated goal to the user; ask — do NOT abort |
| Checks fail after markers removed | Resolution is semantically wrong; re-read both sides' intent (step 2) and fix |
| Conflict markers left in a file | `git grep -nE '^(<{7}|={7}|>{7})'`; resolve remaining hunks before staging |
| Rebase keeps re-conflicting on the same lines | Consider `git rerere`; otherwise resolve each pause carefully — never `--skip` a commit without user approval |
| Binary file conflict | Choose a version explicitly (`git checkout --ours/--theirs <file>`) after confirming intent with the user |

## Handoff Protocol

On a clean tree with green checks → return to `mk:ship` (if mid-ship) or `mk:verify` to
confirm the full build is green before continuing.

## Gotchas

- **`ours`/`theirs` invert between merge and rebase** — during rebase, `ours` is the
  upstream you are replaying onto, not your own work. Confirm direction before picking a side.
- **Marker-free ≠ resolved** — code with no conflict markers can still be semantically
  broken (both sides edited the same logic differently). Step 4's checks are what prove it.
- **`git rebase --continue` pauses repeatedly** — one stop per conflicting commit. It is
  not done until `git status` says so; resolving the first pause is not the end.
