IntentMerge is a production-grade, AST-based CLI tool designed to replace Git's standard line-based merge conflict resolution for TypeScript and React (JSX/TSX) projects.
Traditional Git merging operates on raw text lines. If two developers modify the same lineβeven if one only added formatting and the other changed logicβGit flags a conflict. IntentMerge fixes this by understanding the semantic intent of the code.
By parsing code into an Abstract Syntax Tree (AST), compiling the structural diffs, and applying deterministic resolution rules, IntentMerge guarantees safe automerges and highlights exactly why a conflict is dangerous.
- AST-Based Understanding: We don't compare strings; we compare tree nodes. A change in indentation or trailing commas literally doesn't exist to IntentMerge's diffing engine.
- Intent Classification: We categorize changes. Did the developer rename a variable? Add a JSX prop? Modify a hook dependency array? Each receives a specific label (e.g.,
rename,hook dependency change). - Deterministic Auto-Merge: Safe intersections (like developer A adding a prop
<Button size="lg" />, and developer B adding a prop<Button color="red" />) are mathematically and deterministically merged. - Validation-First: No silent logic corruption. Every successful AST merge is written to disk and immediately checked using
tsc --noEmit. If TypeScript compilation fails, the merge is rolled back to manual intervention. - No Core LLM Hallucinations: Resolution logic is strictly programmatic. LLMs are never used to force an uncontrolled code mutation.
IntentMerge operates through a 6-stage pipeline:
We use @babel/parser to ingest the base file and the two divergent branches (A and B).
- Format Normalization: We strip
loc,start,end, andextraproperties from the Babel AST. This ensures that formatting noise (spaces, line-breaks) is completely ignored during deep equality comparisons.
A recursive tree traversal compares the normalized base AST against versions A and B.
- It produces a unified
AstChangeinterface containing{ nodeType, path, changeType: 'added' | 'removed' | 'modified', baseNode, branchNode }. - Note: The MVP currently relies on indexing for arrays without clear unique identifiers.
Transforms raw node differences into semantic human-readable intents (ChangeClass).
- Maps
JSXAttributechanges toprop modification. - Maps modification of
useEffectarguments tohook dependency change. - Maps
Identifiernode replacements torename. - Maps binary, logical, and execution blocks to
function logic modification.
The intelligence core. It iterates over overlapping change paths and applies a strict safety matrix:
- β
Safe (Auto-merge):
renamevsfunction logic modification. - β
Safe (Auto-merge):
formattingvs anything. - β Safe (Auto-merge): Mutating entirely different JSX props on the same element.
- β Unsafe (Manual):
function logic modificationvsfunction logic modification(both branches changed the inner workings of the exact same function differently). - β Unsafe (Manual):
hook dependency changevshook dependency change.
It outputs a Result containing a confidenceScore and a boolean resolutionType: 'auto-safe' | 'manual-required'.
If the merge is auto-safe, the Generator applies the safely isolated A and B AST modifications back onto the base AST tree. It then uses @babel/generator to emit clean, minified TypeScript code.
Since Babel generator output can be aggressively dense, we pipe the result through Prettier to restore standard developer formatting.
The formatted code is written to a hidden temporary file (.temp-validate-merge.tsx). We spawn a child process running npx tsc --noEmit against it.
- If it passes: The resolution is finalized and written to the output destination.
- If it fails: The CLI throws a strict validation error and alerts the developer.
Clone the repository and install dependencies:
git clone <repo>
cd intentmerge
npm install
npm run build
npm linkNote: npm link allows you to run the intentmerge command globally in your terminal.
Use the CLI to resolve a 3-way conflict. Provide the common ancestor (--base), your current branch (--branchA), the incoming branch (--branchB), and where you want the successfully merged file to go (--output).
npx intentmerge resolve -b base.tsx -a branchA.tsx -c branchB.tsx -o output.tsxOr if installed globally via npm link:
intentmerge resolve -b base.tsx -a branchA.tsx -c branchB.tsx -o output.tsxParsing ASTs...
Diffing base to Branch A...
Diffing base to Branch B...
Classifying differences...
Resolving conflicts...
================ RESOLUTION SUMMARY ================
Confidence Score: 100%
Resolution Type: auto-safe
Safe Changes: 2
Unresolved/Conflicts: 0
====================================================
Applying safe changes & Generating code...
Validating output TS code...
β
Validation passed. Writing resolved file to: /path/to/output.tsx
The resolution matrix and Diffing engine are heavily unit-tested.
- Unit tests: Test suite covering isolated classifications and mock resolution matrices (e.g. asserting that Renames merge with Logic, but overlapping Logic mutations throw conflicts).
npm test- E2E Mock Testing: You can test the actual CLI behavior by running the script against the provided
mockfiles in the repository root:
intentmerge resolve -b mock-base.tsx -a mock-a.tsx -c mock-b.tsx -o res.tsxThis is a Minimum Viable Product demonstrating programmatic semantic intent.
- Array matching: Complex structural additions/removals requiring parent context tracking (e.g., adding a JSX child in the middle of a list of children without a strict ID) currently fall back to index matching which can be noisy. This is flagged in logs.
- Comment nodes conflict: Comments in source code are stored in the AST just like code. If both branches add different comments to the same node (e.g.,
// Branch A: ...and// Branch B: ...), IntentMerge will correctly flag these as conflicts β even if the underlying logic is different. This is by design: a comment describing a change is semantically part of that change. - Cross-File Resolution: IntentMerge currently analyzes isolated, single files. Cross-file refactor dependencies (e.g. changing an exported Type in
A.tsand referencing it inB.ts) are left to standard integration checks. - CSS: Does not support CSS/SCSS module merging.
When a merge is flagged as manual-required, IntentMerge automatically detects whether an LLM is available and offers to explain the conflicts and suggest how to resolve them.
When you see β οΈ Manual conflict resolution required, the CLI will check for providers in order:
| Priority | Provider | Trigger |
|---|---|---|
| 1st | Gemini | GEMINI_API_KEY or GOOGLE_API_KEY env var |
| 2nd | Groq | GROQ_API_KEY env var |
| 3rd | Local LLM | Ollama running on localhost:11434 |
If a provider is found, you'll be prompted:
π€ Would you like LLM advisory via Gemini (GEMINI_API_KEY detected)? (y/n):
If you answer y, the tool sends all three file versions + the classified conflict list to the LLM and prints structured advice:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π§ LLM Advisory (GEMINI)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[Explanation of each conflict and a proposed merge strategy]
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Use Gemini (recommended)
export GEMINI_API_KEY=your_key_here
# Or Groq (free tier available)
export GROQ_API_KEY=your_key_here
# Or run Ollama locally (no API key needed)
ollama serve
ollama pull llama3 # or any model
# IntentMerge auto-detects Ollama on localhost:11434Note: LLM advisory is purely informational. It never automatically applies any code changes β that would violate the no-hallucination guarantee. You always stay in control.
This scenario (from examples/hooks/use-debounce/) demonstrates a case where both branches appear to make "light" changes β but IntentMerge correctly flags 4 conflicts:
Branch A replaced the internal timer variable with a useRef (a more idiomatic React pattern for persisting values across renders without triggering re-renders).
Branch B introduced a safeDelay = Math.max(100, delay) floor and used it in both the setTimeout call and the useEffect dependency array.
Both changes modified useEffect's body at the same array positions (body[1] and body[2]), making them structurally irreconcilable:
| Branch A | Branch B | |
|---|---|---|
body[1] |
if (timeoutRef.current) clearTimeout(...) |
const timer = setTimeout(..., safeDelay) |
body[2] |
timeoutRef.current = setTimeout(...) |
return () => clearTimeout(timer) |
| Deps | [value, delay] |
[value, safeDelay] |
Additionally, both branches added different // Branch X: leading comments to the same AST node β and since comments live in the AST, they were flagged as comment-node conflicts too.
Confidence score: 20% β correctly low because the majority of the function body was in unresolvable conflict.
This is IntentMerge working as designed: it refuses to guess which developer's logic is "more correct."
- β
LLM Advisory Mode: Implemented β uses Gemini, Groq, or local Ollama when
manual-required. - Abstract Object ID mapping: Enhancing the Diff engine to recognize React
keyprops and ASTiddeclarations to completely ignore array index shifts during diffing. - Cross-file resolution: Tracking type exports and usages across files.