conflict-pilot
v0.1.0
Published
Detects merge conflicts, explains what each one actually is, and resolves them once you approve. Zero dependencies, works from any code editor.
Maintainers
Readme
Conflict Pilot
Detects merge conflicts, works out what each one actually is, and resolves it once you approve. Zero dependencies, one small Node package, and it works in any code editor.
── src/app.js · conflict 1/2 · line 2 ─────────────────────────────
── current: main
- import { logger } from './logger';
── incoming: feature/metrics
+ import { metrics } from './metrics';
● 91% high Import block union
Both sides only add imports. Imports are order-independent here, so the
union of both lists (duplicates removed) preserves what each branch needs.
── proposed resolution
│ import { logger } from './logger';
│ import { metrics } from './metrics';
[a] accept [o] yours [t] incoming [b] both [e] edit [s] skip [q] quitNothing is ever written until you press a key.
Install
npm install -g conflict-pilotThen, once:
conflict-pilot setupThat registers Conflict Pilot as your git mergetool, which is what makes it work
in any editor — including editors with no plugin. It prints every git config
line it intends to write and asks before changing anything (--print shows them
without applying, --local scopes them to a single repo).
Or skip installing entirely:
npx conflict-pilotRequires Node 16+ and git. Runs on macOS, Linux and Windows. The cpilot alias is
installed too.
Use it
conflict-pilotThat is the whole workflow: run it in a repo mid-merge or mid-rebase, review each proposal, approve the ones you agree with. Files that end up fully resolved are staged for you.
| Command | What it does |
| --- | --- |
| conflict-pilot | Review every conflict, one approval at a time |
| conflict-pilot --list | Show every conflict and proposal, change nothing |
| conflict-pilot --auto | Apply proposals at ≥90% confidence, review the rest |
| conflict-pilot --yes | Same, but skip everything below the bar instead of asking |
| conflict-pilot --check | Exit 1 if any conflict remains — for hooks and CI |
| conflict-pilot --dry-run | Show what would be written, touch nothing |
| conflict-pilot src/app.js | Just this file |
--help lists the rest.
What it can work out
Each conflict is matched against a set of rules. The one with the highest confidence wins, and the reasoning is always shown.
| Confidence | What it means | | --- | --- | | 100% | Provably identical — no judgement involved | | 90–99% | Structurally safe: the common ancestor proves only one side moved | | 70–89% | Very likely right, but worth a glance | | below 70% | A convenience shortcut — never applied without an explicit keystroke |
Rules include: identical sides, only-one-side-changed, non-overlapping edits
merged three-way, import-block unions (JS/TS, Python, Java, Go, Rust, Ruby, PHP,
C#, C/C++, Swift, Dart, Elixir), package.json dependency unions with semver
tie-breaking, .gitignore-style entry unions, whitespace-only differences, and
changelog bullet unions.
Lockfiles (package-lock.json, yarn.lock, Cargo.lock, go.sum, …) are never
merged line by line. Conflict Pilot tells you to regenerate them and gives you the
command.
Two things that make the proposals better than a guess
It recovers the common ancestor. Most people never set
merge.conflictStyle=diff3, so the conflict in your file shows only two sides —
which makes "who actually changed this?" unanswerable. Conflict Pilot replays the
merge against git's staged blobs to recover the ancestor, so it can tell a real
disagreement from one side simply standing still.
It splits mixed hunks. git reports one conflict per hunk, so a single block routinely mixes something easy (each branch added a different import) with something hard (each branch set the same constant differently) — and the mix makes the whole block look unresolvable. Conflict Pilot re-segments the hunk against the ancestor and resolves the pieces independently:
// git gives you one conflict covering all of this:
<<<<<<< HEAD
import { logger } from './logger';
...
const timeout = 90;
return core(timeout);
=======
import { metrics } from './metrics';
...
const timeout = 30;
return core(timeout, { retries: 3 });
>>>>>>> feature
// Conflict Pilot resolves it to:
import { logger } from './logger';
import { metrics } from './metrics';
...
const timeout = 90; // your change
return core(timeout, { retries: 3 }); // their changeBoth branches' work survives. A block only re-segments when collapsing every conflict to one side yields a byte-identical file both ways, which means local edits you already made are never silently dropped — if you have edited the file, it declines and leaves your version alone.
Editor setup
The CLI works in any editor's integrated terminal with no setup at all. The integrations below add editor-native touches.
Any editor — as your git mergetool
conflict-pilot setup does this for you. It is equivalent to:
git config --global mergetool.conflict-pilot.cmd 'conflict-pilot "$MERGED"'
git config --global mergetool.conflict-pilot.trustExitCode true
git config --global merge.tool conflict-pilotThen git mergetool runs it on each conflicted file, whatever you edit code in.
Recommended for everyone
git config --global merge.conflictStyle zdiff3This keeps the common ancestor in the file. Conflict Pilot can recover it without this, but having it there makes every tool you use smarter, not just this one.
VS Code / Cursor / VSCodium / Windsurf
npm run build:vscode # vendors the engine into the extension
code --install-extension editors/vscodeVS Code already ships Accept Current / Incoming / Both, so the extension does not repeat them. It adds the missing piece above each conflict:
- ⚡ Resolve: Import block union (91%) — applies the proposal in one click
- Why? — explains the reasoning
- A status-bar count of conflicts, and how many are resolvable
- Conflict Pilot: Resolve confident conflicts in this file, which previews every change and asks once before applying
Proposals below conflictPilot.minConfidence always show a preview you must
confirm. Settings: minConfidence, showCodeLens, preferOnFormatting.
Prefer a terminal? editors/vscode/tasks.json has ready-made tasks.
Neovim
{ dir = "/path/to/conflict-pilot/editors/nvim",
config = function() require("conflict-pilot").setup() end }:ConflictPilot reviews everything in a terminal split, :ConflictPilotList
loads conflicts into the quickfix list, :ConflictPilotExplain explains the one
under your cursor, :ConflictPilotFile resolves the confident ones after showing
you what they are.
JetBrains (IntelliJ, WebStorm, PyCharm, GoLand, Rider, …)
Copy editors/jetbrains/conflict-pilot-tools.xml into your IDE's tools/
directory (the file lists the exact path per OS) and restart. Four entries appear
under Tools → External Tools → Conflict Pilot, and you can bind keys to them in
Settings → Keymap.
Sublime Text
Copy editors/sublime/* into Packages/User/. The commands appear in the command
palette under "Conflict Pilot".
Zed, Helix, Emacs, anything else
Use the terminal, or the git mergetool setup above.
Safety
- Nothing is written without your approval.
--autoonly applies proposals at or above the confidence threshold; everything else is still shown to you. - Originals are backed up to
.git/conflict-pilot/backups/before the first write.--no-backupturns this off. - Skipped conflicts keep their markers and the file stays unmerged, so you can quit halfway and come back.
- Your local edits win. Any analysis that would not reproduce your file exactly is discarded rather than applied.
- Ambiguity is left alone. When both sides change the same line differently, there is no proposal — by design.
In CI or a hook
--check exits 1 when any conflict remains, so a pre-commit hook can stop a
commit that still contains markers:
#!/bin/sh
conflict-pilot --check || {
echo "Unresolved conflicts. Run: conflict-pilot"
exit 1
}As a library
const { analyzeFile, discover, writeFile } = require('conflict-pilot');
const found = discover(process.cwd());
for (const entry of found.files) {
const { file } = analyzeFile(entry.absPath, {
relPath: entry.relPath,
repoRoot: found.repoRoot,
});
for (const conflict of file.conflicts) {
const s = conflict.suggestion;
if (s && s.lines && s.confidence >= 0.95) {
conflict.resolution = s.lines; // opt in per conflict
}
}
writeFile(file, { repoRoot: found.repoRoot, stage: true });
}conflict-pilot --json gives the same analysis as machine-readable output; that is
what the editor integrations consume.
Exit codes
0 everything resolved · 1 conflicts remain · 2 error
Development
npm test # 74 tests, including real git merges and rebases
npm run build:vscode # vendor the engine into the extensionThe test suite builds throwaway repos and runs actual git merge operations, so
the parser and strategies are exercised against git's real output rather than
hand-written marker text.
License
MIT
