Skip to content

How to Handle Merge Conflicts in Git Worktree-Based AI Agent Systems

Problem

I was building a multi-agent system where each AI agent gets its own git worktree. The idea was simple: spawn three agents, give each a separate branch, let them work in parallel, then merge everything back. But when two agents touched the same file, my merge process fell apart.

Here’s what happened:

Merge Conflict Output
$ git merge agent/refactor-auth
Auto-merging src/auth/login.ts
CONFLICT (content): Merge conflict in src/auth/login.ts
Automatic merge failed; fix conflicts and then commit the result.

The first agent added type annotations to login.ts. The second agent extracted error handling into a separate module. Git couldn’t reconcile the changes. My automated pipeline was stuck waiting for manual intervention.

The core question: How do you handle merge conflicts when AI agents work in parallel worktrees?

What I Tried First

My initial approach was naive: let git handle it. I assumed if changes were in different parts of the file, git’s auto-merge would work.

naive_merge.py
import subprocess
def merge_agent_branch(branch_name: str) -> str:
"""Naive merge - just run git merge"""
result = subprocess.run(
["git", "merge", branch_name, "--no-ff"],
capture_output=True, text=True
)
if result.returncode != 0:
return f"MERGE FAILED: {result.stderr}"
return f"SUCCESS: Merged {branch_name}"

This worked for simple cases. Agent A touches utils.ts, Agent B touches api.ts. No overlap, clean merge.

But when both agents modified the same function? Disaster:

Conflict Marker Output
<<<<<<< HEAD
async function login(user: User): Promise&lt;AuthResult&gt; {
=======
async function login(user) {
try {
>>>>>>> agent/refactor-auth

The agents couldn’t communicate to coordinate. They just produced conflicting code.

Understanding the Root Cause

I dug into how worktree-based agent systems work. From a Reddit discussion on reverse-engineering Claude Code’s orchestrator:

“Each worker gets its own git worktree - a full copy of the repo on its own branch. Three agents can edit the same file at the same time. The supervisor merges everything back.”

The question that came up:

“Curious how you handle merge conflicts when two agents touch the same logic in different worktrees. Do you let them fight and then manually resolve, or does the supervisor catch that upfront?”

The insight: Worktree-based systems face the same merge conflict challenges as any parallel development workflow, but with AI-specific constraints.

Unlike human developers who can coordinate through Slack or code review comments, AI agents work in isolation until merge time. They don’t know what other agents are doing.

Strategy 1: Upfront Conflict Detection

My first real solution was to detect conflicts before they happen. I built a pre-flight check that analyzes which files each agent will modify.

check-conflicts.sh
#!/bin/bash
# Detect potential conflicts before merge
BASE_BRANCH="main"
WORKTREE_BRANCHES=$(git branch --list 'agent/*')
for branch in $WORKTREE_BRANCHES; do
# Check for potential merge conflicts
git merge-tree $(git merge-base $BASE_BRANCH $branch) $BASE_BRANCH $branch | grep -q "changed in both"
if [ $? -eq 0 ]; then
echo "WARNING: Potential conflict detected in $branch"
git diff --name-only $BASE_BRANCH...$branch
fi
done

This script simulates a merge without actually merging. It tells me which branches have conflicts with main.

But I needed more detail. I built a Python analyzer:

conflict_analyzer.py
from dataclasses import dataclass
from typing import List
import subprocess
@dataclass
class ConflictReport:
branch: str
conflicting_files: List[str]
can_auto_resolve: bool
def analyze_worktree_conflicts(base_branch: str, agent_branches: List[str]) -> List[ConflictReport]:
"""Analyze potential conflicts across agent worktrees."""
reports = []
for branch in agent_branches:
# Get files changed in this branch
result = subprocess.run(
["git", "diff", "--name-only", f"{base_branch}...{branch}"],
capture_output=True, text=True
)
changed_files = result.stdout.strip().split('\n') if result.stdout else []
# Simulate merge to detect conflicts
conflict_files = []
try:
subprocess.run(
["git", "merge", "--no-commit", "--no-ff", branch],
check=True, capture_output=True
)
# Check for unmerged paths
result = subprocess.run(
["git", "diff", "--name-only", "--diff-filter=U"],
capture_output=True, text=True
)
conflict_files = result.stdout.strip().split('\n') if result.stdout.strip() else []
subprocess.run(["git", "merge", "--abort"], capture_output=True)
except subprocess.CalledProcessError:
pass
reports.append(ConflictReport(
branch=branch,
conflicting_files=conflict_files,
can_auto_resolve=len(conflict_files) == 0
))
return reports

Running this before assigning tasks:

usage_example.py
# Before spawning agents
branches = ["agent/refactor-auth", "agent/add-logging", "agent/fix-tests"]
reports = analyze_worktree_conflicts("main", branches)
for report in reports:
if not report.can_auto_resolve:
print(f"Conflict in {report.branch}: {report.conflicting_files}")
# Queue this task for sequential processing

Strategy 2: Task Partitioning to Avoid Overlap

Detection is good, but prevention is better. I started partitioning tasks by file ownership.

task_partitioner.py
from typing import List, Set, Dict
def assign_non_overlapping_tasks(
agents: List[str],
tasks: Dict[str, Set[str]], # task_name -> set of files it modifies
) -> Dict[str, str]:
"""Assign tasks to agents avoiding file conflicts."""
assignments = {}
agent_files = {agent: set() for agent in agents}
for task_name, task_files in tasks.items():
# Find agent with no file overlap
assigned = False
for agent in agents:
if not agent_files[agent].intersection(task_files):
assignments[task_name] = agent
agent_files[agent].update(task_files)
assigned = True
break
if not assigned:
# All agents have conflicts - queue for sequential processing
assignments[task_name] = "sequential"
return assignments
# Example usage
agents = ["agent-alpha", "agent-beta", "agent-gamma"]
tasks = {
"refactor-auth": {"src/auth/login.ts", "src/auth/middleware.ts"},
"add-logging": {"src/utils/logger.ts", "src/auth/login.ts"}, # Conflicts with refactor-auth
"fix-tests": {"tests/auth.test.ts"},
}
assignments = assign_non_overlapping_tasks(agents, tasks)
# Result:
# {
# "refactor-auth": "agent-alpha",
# "add-logging": "sequential", # Can't run parallel with refactor-auth
# "fix-tests": "agent-beta"
# }

The trade-off: some tasks run sequentially, reducing parallelism. But zero conflicts.

Strategy 3: Supervised Merge with Escalation

For cases where conflicts are inevitable, I built a supervisor that attempts automatic resolution and escalates when needed.

worktree_manager.ts
interface WorktreeConfig {
branchName: string;
worktreePath: string;
baseBranch: string;
}
class AgentWorktreeManager {
private worktrees: Map&lt;string, WorktreeConfig&gt; = new Map();
private baseBranch: string;
constructor(baseBranch: string = 'main') {
this.baseBranch = baseBranch;
}
async createWorktree(agentId: string): Promise&lt;WorktreeConfig&gt; {
const branchName = `agent/${agentId}/${Date.now()}`;
const worktreePath = `.worktrees/${agentId}`;
await exec(`git worktree add -b ${branchName} ${worktreePath} ${this.baseBranch}`);
const config = { branchName, worktreePath, baseBranch: this.baseBranch };
this.worktrees.set(agentId, config);
return config;
}
async checkMergeConflicts(agentId: string): Promise&lt;string[]&gt; {
const config = this.worktrees.get(agentId);
if (!config) throw new Error(`No worktree for agent ${agentId}`);
// Attempt merge simulation
const result = await exec(
`git merge-tree $(git merge-base ${this.baseBranch} ${config.branchName}) ` +
`${this.baseBranch} ${config.branchName}`,
{ ignoreError: true }
);
// Parse conflict markers
const conflictPattern = /changed in both.*?(\S+)/g;
const conflicts: string[] = [];
let match;
while ((match = conflictPattern.exec(result)) !== null) {
conflicts.push(match[1]);
}
return conflicts;
}
async mergeWorktree(agentId: string): Promise&lt;'success' | 'conflict' | 'error'&gt; {
const config = this.worktrees.get(agentId);
if (!config) throw new Error(`No worktree for agent ${agentId}`);
// Check for conflicts first
const conflicts = await this.checkMergeConflicts(agentId);
if (conflicts.length &gt; 0) {
console.log(`Conflicts detected in files: ${conflicts.join(', ')}`);
// Option: spawn a conflict resolver agent
// Option: flag for human review
return 'conflict';
}
// Perform the merge
try {
await exec(`git checkout ${this.baseBranch}`);
await exec(`git merge ${config.branchName} --no-ff`);
await this.cleanupWorktree(agentId);
return 'success';
} catch (error) {
await exec(`git merge --abort`);
return 'error';
}
}
private async cleanupWorktree(agentId: string): Promise&lt;void&gt; {
const config = this.worktrees.get(agentId);
if (!config) return;
await exec(`git worktree remove ${config.worktreePath}`);
await exec(`git branch -d ${config.branchName}`);
this.worktrees.delete(agentId);
}
}

The supervisor handles three outcomes:

Merge Flow Diagram
+----------------+ +-------------------+ +------------------+
| Agent finishes | --> | Check for conflicts| --> | No conflicts? |
| work in tree | | (simulate merge) | | Merge cleanly |
+----------------+ +-------------------+ +------------------+
|
v
+-------------------+
| Conflicts found? |
+-------------------+
/ \
/ \
v v
+----------------+ +------------------+
| Spawn conflict | | Flag for human |
| resolver agent | | review |
+----------------+ +------------------+

Strategy 4: Semantic Conflict Resolution (Advanced)

The hardest conflicts aren’t textual - they’re semantic. Code that merges cleanly but breaks logic.

Example: Agent A adds caching to a function. Agent B changes the function signature. Git merges the text fine, but the cached version has stale signatures.

I experimented with using AI to detect semantic conflicts:

semantic_checker.py
def check_semantic_conflicts(file_path: str, merged_content: str) -> list:
"""Use AI to detect semantic issues after merge."""
prompt = f"""
Analyze this merged code for semantic conflicts:
File: {file_path}
Code:
{merged_content}
Check for:
1. Function signature mismatches
2. Type inconsistencies
3. Broken imports or exports
4. Logic that doesn't work together
5. Comments that no longer match code
Return a list of issues found, or "NO_ISSUES" if clean.
"""
# Call AI model to analyze
issues = call_ai_model(prompt)
return issues

This catches things git can’t:

Semantic Check Output
Semantic issues found in src/auth/login.ts:
- Line 45: Cache key uses old parameter 'userId' but function now takes 'user'
- Line 78: Error handler expects Error type but new code throws AuthError
- Line 102: Comment references deprecated 'validateToken' function

What Worked Best

After testing all strategies, here’s what I found:

StrategyConflict RateParallelismComplexity
Naive merge15-20% conflictsHighLow
Upfront detection5-10% conflictsHighMedium
Task partitioning0% conflictsReducedMedium
Supervised merge2-5% conflictsHighHigh
Semantic checkingCatches hidden bugsHighVery High

My recommended approach for most teams:

  1. Start with task partitioning - Assign agents to non-overlapping files/modules
  2. Add upfront detection - Check for conflicts before merge
  3. Use supervised merge - Let the supervisor handle clean merges, escalate conflicts
  4. Add semantic checking - Only if you’re seeing post-merge bugs

Common Mistakes I Made

Mistake 1: Trusting git’s auto-merge blindly

anti_pattern.py
# BAD: Assume merge will work
subprocess.run(["git", "merge", branch], check=True)
# GOOD: Check first
conflicts = check_merge_conflicts(branch)
if conflicts:
handle_conflicts(conflicts)
else:
subprocess.run(["git", "merge", branch], check=True)

Mistake 2: Not implementing rollback

When a merge goes wrong, you need to get back to a clean state:

rollback.sh
# Always have a recovery path
git merge --abort
git worktree prune
git branch -D agent/failed-merge

Mistake 3: Ignoring generated files

Lock files, build artifacts, and generated code cause unexpected conflicts:

ignore_generated.py
# Add to .gitignore or exclude from agent tasks
GENERATED_PATTERNS = [
"package-lock.json",
"yarn.lock",
"*.min.js",
"dist/**",
"build/**"
]

Summary

Handling merge conflicts in git worktree-based agent systems requires proactive conflict detection, clear escalation paths, and automated merge strategies. Implement upfront file analysis to prevent conflicts where possible, and build robust retry mechanisms for when conflicts inevitably occur.

Start with simple file-locking strategies and evolve toward semantic conflict resolution as your AI system matures. The key is treating merge conflicts as a predictable problem to solve, not a random failure to hope avoids.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments