resolve-conflicts
Resolve merge conflicts.
- License 许可
- MIT
- 最近更新
- 2026-11-15
- Stars
- ★ 4
- Forks
- 1
01 前言 Front Matter
- name
- resolve-conflicts
- description
- Resolve merge conflicts.
- allowed-tools
- [ "Bash", "Read", "Edit", "Write" ]
- user-invocable
- true
03 SKILL.md 正文
resolve-conflicts — consolidate the best of both branches
A merge conflict means both branches edited the same region — and each side
changed it for a reason. The right resolution almost always keeps the intent
of both sides. The default failure mode is to grab one side wholesale
(--ours / --theirs) and silently throw the other branch's work on the
floor. Don't. Consolidate, don't choose.
When this fires
- Any git operation stops on a conflict:
git merge,git rebase,git cherry-pick,git stash pop,git pull(merge or rebase),git revert. The tell isCONFLICT (content): Merge conflict in …andUnmerged paths:ingit status. - "resolve the conflicts", "fix the merge conflicts", "I have a merge conflict", "consolidate the best of both branches", "help me merge these branches", "the branch won't merge cleanly".
- Delegated from another skill:
sync-pr-branchstep 5 ("resolve any conflicts fully"),clean-branches' rebase-onto-main step, or a stackedgiiMR. When one of those stops on a conflict, run this.
Verify before you act — don't trust mergeable alone
Before claiming and fixing a PR flagged by a cascade conflict scan
(post-merge step 1.5, ardi's opportunistic sweeps, wrap-up step 1), get
ground truth locally instead of acting on the API field directly:
git fetch origin main <branch> -q # FETCH
git merge-tree --write-tree origin/main origin/<branch> # git >=2.38; no worktree/checkout needed
echo "exit: $?" # 0 = clean merge (no action needed); 1 = real conflict exists
Two failure modes this catches:
mergeable == "UNKNOWN"is not "no conflict." GitHub returnsUNKNOWNright after a push or a sibling PR's merge, while it's still computing — sometimes for minutes. A cascade scan that filters onlymergeable == "CONFLICTING"silently skips a PR stuck inUNKNOWNeven when it's genuinely conflicting. TreatUNKNOWNthe same asCONFLICTING: a candidate to verify, not a PR to skip.mergeable == "CONFLICTING"(or adirtymergeable_state) can itself be stale. GitHub computes it against whatevermainsnapshot it last saw — if a sibling PR merged since, the flag may still say conflicting even though a fresh merge against currentmainis clean. Acting on a stale flag risks wasted resolution effort. (Seen on ai-config#372: flaggeddirty, butgit merge-treeagainst currentmaincame back clean — no manual resolution was needed, just agit merge+ push.)
If git merge-tree exits clean, there's nothing to resolve — skip the PR (or,
if you want the branch to reflect the now-current main, do a plain
git merge origin/main && git push, no conflict markers involved). Only
proceed to the consolidation procedure below when it reports a real conflict.
Check git --version before trusting the exit code — on git <2.38 this
command fails a different way that a naive check misreads. --write-tree
mode doesn't exist before 2.38; on an older install the command errors with
fatal: unknown rev --write-tree (a non-zero exit, same as a real conflict)
or, if scripted with a text-based check instead of the exit code, an error
message that a naive grep -qi conflict fails to match — silently
misclassifying every single PR in a batch scan as clean (a false negative
that defeats the whole scan; hit across a 5-PR cascade scan on ai-config,
2026-07-03). Confirm git --version is 2.38+ before relying on this
command's exit code at all. On an older git, use the legacy 3-arg form
instead (same read-only guarantee, no worktree/checkout):
base=$(git merge-base origin/main origin/<branch>)
git merge-tree "$base" origin/main origin/<branch> # git <2.38 (any version)
# conflict markers ("<<<<<<<") in the output = real conflict; empty/clean = no conflict
Core principle: consolidate the best of both branches
Each conflict hunk has three inputs — the common base, our side, and their side. Picking a winner is right only when one side's change is genuinely the same edit, or strictly subsumes the other. Otherwise the merge should contain a synthesis that honors both:
- Two different features touching neighbouring lines → keep both edits.
- A refactor on one side + a bugfix on the other → apply the bugfix to the refactored code.
- A rename on one side + an edit to the old name on the other → carry the edit onto the new name.
- A conflict over a string your branch turned into one shared definition
(several per-consumer copies of a text, now read from one variable or file)
is not a one-hunk decision.
Main's edit may have been meant for only some of the consumers.
Before folding main's wording into the shared string,
list every file or chunk that reads it (
grepfor the name), and read what main did to each consumer. If main treated them differently, split the string. (Morrison-Lab/lds#2208: main reworded the alt text of the interactive figures and deliberately kept the old wording for the static matplotlib figure; putting the new wording into the shared string changed both, and the review blocked the merge head, ai-config#5270.) CONFLICT (add/add)on the same newly-added file (common with sibling PRs) → compare:2:vs:3:first; keep the side that carries the newer baseline (oftenmainafter a sibling PR merged), then manually re-apply any newer deltas from the other side (review fixes, wording corrections, version pins) before staging.
If you can't see why a side changed the hunk, you're not ready to resolve it — go read the history first (step 2).
Procedure
-
Survey the battlefield. Know which operation you're in (the semantics of "ours"/"theirs" depend on it — see the warning below) and list the conflicted files:
git status # header names the operation git diff --name-only --diff-filter=U # just the unmerged paths -
Understand BOTH sides before editing a single hunk. For each conflicted file, look at the three-way picture and why each side diverged:
git log --merge --oneline -- <file> # commits from both sides (during a MERGE) git show :1:<file> # BASE (common ancestor) git show :2:<file> # OURS (HEAD / current side) git show :3:<file> # THEIRS (incoming side)The
:1:/:2:/:3:index stages work in any conflicted operation.git log --mergeis merge-only; for the others, inspect the incoming side directly via the operation's ref —git show MERGE_HEAD(merge),REBASE_HEAD(rebase),CHERRY_PICK_HEAD(cherry-pick),REVERT_HEAD(revert), or the keptstash@{0}entry for a stash pop (git stash show -p stash@{0}). Ask: what problem was each side solving? Different features? One refactor + one fix? A move/rename vs an in-place edit? The answer dictates the merge. -
Consolidate. Edit the working file into a version that keeps the valuable change from both sides, then delete every conflict marker (
<<<<<<<,=======,>>>>>>>). Take one side wholesale only when you've confirmed the other side has nothing worth keeping — and note that you did.Record a decision for every hunk. Draft the table while you resolve, and finish it before you finish the operation. For each conflict hunk, write one row: what you kept from each side, what you dropped (from which side) and why, and what you modified and why. Write each side's real name, its branch, commit or stash entry, in place of
<side A>and<side B>, never "ours" or "theirs", since those flip in a rebase (see the warning below). For a merge, put the table in the merge commit message. When the work is on a PR, also post it in the PR comment that announces the result, since a squash merge discards the merge message and a rebase, cherry-pick, revert or stash pop leaves none. With no PR, put it in your report to the user. A reviewer can then check each choice against both sides without redoing the operation. In the table,<n>is the hunk's number within the file, counted from 1, and<lines>is a line range (12-18), never the text itself, which would break the table row. The range is in the resolved file, except under "Dropped", where it is in the named side's version of the file. Write one row per dropped range, so a hunk that drops lines from both sides gets two rows; repeat<path>:<n>in each row, and put the Kept and Modified ranges on the first row only, withnonein the others. Writenonein any empty cell. For a conflict with no text hunks (delete/modify, rename, binary), writewhole filefor<n>and<lines>:File:hunk Kept from <side A>Kept from <side B>Dropped (side, why) Modified (why) <path>:<n><lines or "none"><lines or "none"><lines>(<side>,<reason>)<lines>(<reason>)A row with something in "Dropped" and no reason is the defect this table exists to expose: content that either side added may be dropped only by a decision you can state (Ezra, 2026-10-23, ai-config#4716).
Also check whether the merged result duplicates content that already exists elsewhere in the file — a separate problem from the textual conflict itself. This can arise two ways: a single incoming PR may internally copy a sentence into a new section (the merge is clean, but the result now says the same thing twice), or both branches may independently add equivalent sentences in different sections (a cross-branch variant with the same symptom). In both cases
git merge/merge-treereports zero conflicts even though the result contains duplication — sometimes with a pronoun or reference that only made sense in its original location. Grep the merged file for the distinctive phrase from each side's addition before finishing; if it already appears elsewhere, keep one copy (usually the version with the clearer standalone referent) and drop the redundant one. (ai-config#446: the merge itself was clean, but the incoming PR's own diff had separately duplicated a CLAUDE.md sentence verbatim into a different section, where "this policy" lost its antecedent — the intra-PR variant, caught by the@claudereview bot as a distinct finding from the conflict resolution.) -
Prove no markers (or whitespace damage) slipped through:
git diff --check # flags leftover markers + whitespace errors grep -rnE '^(<<<<<<< |======= *$|>>>>>>> )' . --exclude-dir=.git # precise markers, portable -E -
Run the repo's pre-commit checks before committing the resolution. A merge where each side compiled separately can still break combined. Build / lint / test / spellcheck per the current repo (use its
render/lint/spell/testskills if it ships them). Don't commit a resolution that fails checks. -
Stage the resolved files, then finish the operation — never
--abortor--skipaway a conflict you were asked to resolve. The finish command matches the operationgit statusnames, not the command you typed (agit pullis a merge or a rebase underneath — check which):git add <file> … git commit # merge (message prefilled from MERGE_MSG) git rebase --continue # rebase (incl. `git pull --rebase`) git cherry-pick --continue # cherry-pick git revert --continue # revert (message prefilled) # stash pop: no commit/continue — `git add` the resolution, then `git stash drop`. # A conflicted pop does NOT auto-drop the stash, and dropping it is # independent of committing: the resolved changes stay as working-tree edits.
⚠️ "ours" and "theirs" flip between merge and rebase
- In a merge (
git merge Xon branchB):ours/:2:=B(where you are),theirs/:3:=X(being merged in). - In a rebase (
git rebase Xon branchB): rebase replays your commits ontoX, soours/:2:=X, the branch you're landing on, andtheirs/:3:= your own commit being replayed. The opposite of a merge.
Confirm which physical branch is "ours" before you reach for --ours /
--theirs — getting this backwards silently keeps the wrong side. (This is
why the skill defaults to consolidating both rather than picking a side.)
Relationship to other skills
sync-pr-branch(aliasessync/merge-main/resync-branch) — its step 5 says "resolve any conflicts fully"; this is that step's how-to. Run it when that sync stops on a conflict, then return to finish the sync.clean-branches— when rebasing a stale branch ontomainhits a non-trivial conflict, resolve it here instead of skipping the branch.gii/split-concerns— stacked or parallel MRs are the usual source of conflicts; this is how you clear them once they collide.check-history— run before implementing to avoid creating avoidable conflicts/regressions in the first place; this skill cleans up the ones that still happen.session-lock— not this skill. It is about two AI sessions clobbering one local checkout (a locking concern), not git content conflicts. Different sense of "conflict."
Anti-patterns
- ❌ Blind
git checkout --ours/--theirs <file>(orgit merge -X ours/theirs) without confirming the dropped side had nothing worth keeping — the classic silent-data-loss move. The hookhooks/warn-one-sided-conflict-resolution.pywarns on these commands (and onrestore,merge-file,rebase,cherry-pick,revertandpullwith the same options). It also warns ongit merge -s ours, which drops the other branch's changes entirely. - ❌ Resolving a hunk before understanding why each side changed it.
- ❌ Finishing a merge, rebase, cherry-pick, revert or stash pop with no per-hunk keep/drop/modify record (step 3), so nobody can tell a deliberate drop from a lost line.
- ❌ Carrying the wording from one conflicting hunk into a string that several consumers share, without reading main's version of each consumer.
- ❌ Mixing up "ours"/"theirs" in a rebase (they're reversed vs a merge).
- ❌ Committing with conflict markers still in a file — always
git diff --check. - ❌ Committing the resolution without running the repo's build/lint/test checks.
- ❌
git rebase --skip/git merge --abortto dodge a conflict you were asked to resolve. - ❌ Force-pushing a half-resolved merge/rebase.
- ❌ Restoring a whole file from a pre-merge commit
(
git show <old>:<path> > <path>) to recover one entry --- that reverts every other change the file has taken, including everything the merge just brought in. Rebuild the resolution from the conflict stages (:1:/:2:/:3:, above) and swap in only the hunk you came for. Note thatgit show :<path>is not a safe baseline here. While the path is conflicted it has no stage 0 and the command fails, and once you have staged a resolution stage 0 holds your content --- so if the bad restore is what you staged, that is what comes back. Seebatch-merge-and-resolve's "The recovery has a silent failure mode of its own, and no merge produces it", which is why the batch pass's per-merge re-check never surfaces this one. - ❌ Rewriting a file from scratch with no conflict in sight,
such as rebasing a repo's instruction files on another repo's.
That is a one-sided merge of the old file, and it drops content the same way,
with no markers to warn you.
Before pushing, list what the old version had
(
git diff <base> -- <path>, removed lines), and confirm each substantive line either moved somewhere or was deliberately cut. On 2026-09-28 a rebuild of Morrison-Lab/mln'sCLAUDE.mdandAUTHORING.mdon rme's layout dropped repository roles, a CI deadlock's symptoms, conversion history and rule links until that audit restored them.
04 同仓库更多 Skills
send-notes
Send content to Morrison-Lab notes repos.
stack-prs
Stack dependent PR on another.
finish-wave
Finish the current wave; start no new one.
advisor
Read-only second opinion from a different model family.
rec
Provide recommendations for choices.
push
Pre-push check for active sessions.