How file-level ownership eliminated all merge conflicts (but not integration bugs)
Part 4 of the Multi-Agent Development Series
- Part 1: Can 5 Claude Code Agents Work Independently?
- Part 2: The Reality of "Autonomous" Multi-Agent Development
- Part 3: Property-Based Testing with Hypothesis
TL;DR
We achieved 100% auto-merge success rate across 5 parallel PRs. Zero manual conflict resolution. The secret? File-level ownership - each PR creates NEW files or owns completely separate sections.
But here's the twist: Zero merge conflicts ≠ Zero integration work.
Integration still took 6 hours. 5 of those hours were AI debugging, not merging. File-level ownership solves the merge conflict problem, not the integration correctness problem.
The lesson: The 80/20 rule applies. 80% of parallel development success comes from one design decision (file ownership). The remaining 20% is the hard work (making code actually work together).
The Problem: Merge Conflicts Kill Parallel Work
Traditional parallel development:
Developer A: Modifies user_service.py lines 100-150
Developer B: Modifies user_service.py lines 120-180
Result:
<<<<<<< HEAD (A's changes)
def get_user(user_id):
# A's implementation
return db.query(User).filter_by(id=user_id).first()
=======
def get_user(user_id):
# B's implementation
return cache.get(f"user:{user_id}") or db.query(User).get(user_id)
>>>>>>> feature/b (B's changes)
Merge conflict! Someone (even AI) has to manually resolve it.
The Cost of Merge Conflicts
Time per conflict:
- Understand both changes: 5-15 minutes
- Resolve manually: 5-10 minutes
- Test the merge: 5-10 minutes
- Total: 15-35 minutes per conflict
In parallel development with 5 PRs:
- Potential conflicts: 10-50 (depending on overlap)
- Resolution time: 2.5 - 30 hours
- Kills the benefit of parallel work!
Our experiment:
- Expected conflicts (traditional approach): 10-20
- Actual conflicts (zero-conflict architecture): 0
- Time saved: ~10 hours
The Solution: File-Level Ownership
Core principle: Each PR owns completely separate files.
Design rules:
- CREATE new files instead of modifying shared files
- PARTITION existing files carefully (non-overlapping sections)
- DEFER shared changes to final integration PR
How We Applied It
| PR | What It Created | What It Modified | Conflict Risk |
|---|---|---|---|
| PR-1: Test Builders |
tests/test_video_moderation/test_builders.py (NEW) |
None | ✅ Zero |
| PR-2: Budget Tests |
tests/test_video_moderation/unit/test_budget_allocation.py (NEW) |
video_moderator.py (production code) |
✅ Zero (only PR touching this code) |
| PR-3: Segment Tests |
tests/test_video_moderation/unit/test_segment_duration.py (NEW) |
None | ✅ Zero |
| PR-4: Property Tests |
tests/test_video_moderation/simulation/test_properties.py (NEW) |
None | ✅ Zero |
| PR-5: Placement Tests |
tests/test_video_moderation/simulation/test_edge_cases.py (section) |
Only this file, only new section | ✅ Zero |
| PR-6: Integration | Cross-PR integration tests (NEW) | Import statements (deferred to end) | ⚠️ Medium (but sequential) |
Key insight: File creation is conflict-free. Different files = different git objects = zero overlap.
The Design Patterns
Pattern 1: Create New Test Files
Instead of:
# Modifying existing test_video_moderator.py (shared by multiple PRs)
# PR-1 adds tests here
# PR-2 adds tests here
# PR-3 adds tests here
# → CONFLICT!
Do this:
# PR-1 creates:
tests/test_video_moderation/test_builders.py
# PR-2 creates:
tests/test_video_moderation/unit/test_budget_allocation.py
# PR-3 creates:
tests/test_video_moderation/unit/test_segment_duration.py
# NO CONFLICTS - different files!
Benefit:
- Each PR owns its file completely
- Modifications don't overlap
- Git handles trivially (no merge needed, just add both files)
Pattern 2: Partition Existing Files by Section
When you MUST modify an existing file:
# test_edge_cases.py - Existing file
# Section 1: Staleness tests (existing)
class TestStalenessDetection:
... # Don't touch these
# Section 2: Placement change tests (PR-5 OWNS this section)
class TestPlacementChanges:
# PR-5 adds all tests here
def test_placement_change_triggers_immediate_check(self):
...
def test_stale_result_from_lower_risk_placement(self):
...
# No other PR modifies TestPlacementChanges → zero conflict
Key: Clearly delineate sections. Use class-level organization.
Pattern 3: Defer Cross-Cutting Changes
Some changes affect ALL files (example: import statement updates).
Instead of having each PR update imports:
# PR-1 updates: from builders import MH, CH
# PR-2 updates: from builders import TestPolicy
# PR-3 updates: from builders import SAFE, UNSAFE
# → All touching same import lines → CONFLICT!
Defer to integration PR:
# PR-1, PR-2, PR-3: Don't update imports in existing files
# PR-6 (integration): Update ALL imports at once
# No conflicts because it runs AFTER all PRs merged
Tradeoff: Existing tests may use old helpers temporarily. But zero conflicts.
The Results: 100% Auto-Merge
Merge Timeline
| Time | Event | Conflicts | Resolution Time |
|---|---|---|---|
| Hour 8 | Merge PR-1 (test builders) | 0 | 0 minutes ✅ |
| Hour 12 | Merge PR-2 (budget tests) | 0 | 0 minutes ✅ |
| Hour 16 | Merge PR-3 (segment tests) | 0 | 0 minutes ✅ |
| Hour 20 | Merge PR-5 (placement tests) | 0 | 0 minutes ✅ |
| Hour 24 | Merge PR-4 (property tests) | 0 | 0 minutes ✅ |
| Total | 5 PRs merged | 0 | 0 minutes ✅ |
100% auto-merge success rate!
Git Operations
Typical merge:
$ git checkout integration-branch
$ git pull origin pr-2-branch
Auto-merging complete.
Fast-forward merge (no conflicts).
tests/test_video_moderation/unit/test_budget_allocation.py | 145 +++++++
1 file changed, 145 insertions(+)
create mode 100644 tests/test_video_moderation/unit/test_budget_allocation.py
$ git push
Done!
Every single merge looked like this. No conflict markers. No manual resolution. Just git doing its job.
What Zero-Conflict Doesn't Solve
Here's where it gets interesting.
Total integration time: 6 hours
- Merge operations: 30 minutes (git commands, pushing)
- Debugging: 5 hours (making code actually work)
Integration still involved:
1. API Mismatches (2 hours)
Problem: Agents assumed APIs that don't exist.
Example:
# PR-1 assumed:
result.details.classification == VideoModerationClassification.SAFE
# Reality:
result.details.is_appropriate == True # Different field name!
Impact: 13 tests failed due to field name mismatches.
Fix: Human investigated actual model structure, corrected tests.
Lesson: File-level ownership prevents merge conflicts, but doesn't verify API correctness.
2. Cross-PR Integration Issues (1 hour)
Problem: PR-4 changed tier ordering (fairness-first), broke PR-2's tier priority tests.
Example:
# PR-2 tested (before PR-4):
def test_tier1_beats_tier2():
"""Tier 1 (staleness) should beat Tier 2 (fairness)"""
# Passed with original tier ordering
# After PR-4 merged (fairness-first):
# Same test now fails!
# Tier 2 (fairness) now HIGHER priority than Tier 1 (staleness)
Impact: 3 tier priority tests failed after PR-4 integration.
Fix: Updated tests to match new tier ordering.
Lesson: Zero merge conflicts doesn't mean zero integration testing.
3. Missing Features (30 minutes)
Problem: PR-3 wrote tests for features never implemented.
Example:
# PR-3 tested:
assert result.recommended_segment_duration_seconds == 3.0
# Reality: Field doesn't exist in model!
Impact: 4 tests skipped (feature not implemented).
Fix: Marked tests as skipped with clear reason.
Lesson: Integration still needs to verify feature completeness.
4. Test Isolation Issues (1.5 hours)
Problem: Property tests (PR-4) occasionally failed due to shared state.
Example:
# test_properties.py uses hypothesis random generation
# Sometimes generated data conflicted with test database state
# Flaky failures: passed locally, failed in CI
Impact: 5 property tests showing intermittent failures.
Fix: Improved test isolation (separate database, better cleanup).
Lesson: Parallel test development still needs integration testing for isolation.
The 80/20 Breakdown
80% of integration success came from:
- File-level ownership (zero merge conflicts)
- Centralized test builders (shared infrastructure early)
- Clear PR scope (each PR knew exactly what it owned)
20% of effort spent on:
- Fixing API mismatches (13 failures)
- Communication overhead (FEEDBACK files)
- Progress tracking (status documents)
- Cross-PR integration (tier ordering conflicts)
Key insight: Architecture solved the easy problem (conflicts). Humans still needed to solve the hard problem (correctness).
The Real Cost of Integration
| Activity | Time | % of Total |
|---|---|---|
| Git operations (fetch, merge, push) | 30 min | 8% |
| Running tests (pytest execution) | 30 min | 8% |
| Debugging API mismatches | 120 min | 33% |
| Fixing cross-PR issues | 60 min | 17% |
| Verifying feature completeness | 30 min | 8% |
| Test isolation debugging | 90 min | 25% |
| Total | 360 min (6 hours) | 100% |
Merge conflicts: 0% of integration time (architecture win!)
Debugging: 83% of integration time (hard work)
The lesson: Zero-conflict architecture is necessary but not sufficient. The hard work is making code work together, not merging cleanly.
Design Patterns for Zero-Conflict Architecture
Pattern 1: Feature Flags Over Shared Changes
Problem: Multiple PRs need to modify shared configuration.
Traditional approach:
# config.py (shared file)
# PR-1 adds: ENABLE_FEATURE_A = True
# PR-2 adds: ENABLE_FEATURE_B = True
# PR-3 adds: ENABLE_FEATURE_C = True
# → CONFLICT on same file!
Zero-conflict approach:
# PR-1 creates: features/feature_a.py
ENABLE_FEATURE_A = True
# PR-2 creates: features/feature_b.py
ENABLE_FEATURE_B = True
# PR-3 creates: features/feature_c.py
ENABLE_FEATURE_C = True
# config.py (modified by integration PR only):
from features import feature_a, feature_b, feature_c
Pattern 2: Subdirectory Per PR
Organize by work stream:
tests/test_video_moderation/
├── unit/
│ ├── test_budget_allocation.py # PR-2 owns this
│ └── test_segment_duration.py # PR-3 owns this
├── simulation/
│ ├── test_properties.py # PR-4 owns this
│ └── test_edge_cases.py # PR-5 owns section
├── test_builders.py # PR-1 owns this
└── integration/
└── test_cross_pr_integration.py # PR-6 owns this
Benefit: Clear ownership, easy to see what each PR touched.
Pattern 3: Interface Files Over Implementation Sharing
Problem: Multiple PRs need shared test utilities.
Conflict-prone:
# test_utils.py (everyone modifies this)
# PR-1 adds: def make_participant(...)
# PR-2 adds: def make_policy(...)
# PR-3 adds: def make_result(...)
# → CONFLICTS!
Zero-conflict:
# PR-1 creates: test_builders.py with ALL builders
# Other PRs IMPORT from test_builders, don't modify it
# If they need new builders, they ADD to their own files
# Integration PR consolidates duplicates later
Tradeoff: Temporary duplication OK, consolidate in integration.
Pattern 4: Versioned Shared State
Problem: Multiple PRs need test database setup.
Conflict-prone:
# conftest.py (shared pytest fixtures)
# Everyone adds their fixture here
# → CONFLICTS!
Zero-conflict:
# conftest.py (stays minimal, provides base fixtures)
@pytest.fixture
def base_db():
...
# PR-2 creates: conftest_budget.py
@pytest.fixture
def budget_test_db(base_db):
# Extend base for budget tests
...
# PR-3 creates: conftest_segment.py
@pytest.fixture
def segment_test_db(base_db):
# Extend base for segment tests
...
# Integration: Move fixtures back to main conftest if widely used
When Zero-Conflict Architecture Breaks Down
Zero-conflict works best for:
- ✅ New feature development (create new files)
- ✅ Test additions (new test files)
- ✅ Independent modules (low coupling)
- ✅ Parallel refactoring (different areas)
Zero-conflict struggles with:
- ❌ Cross-cutting concerns (logging, error handling)
- ❌ Shared interfaces (API contracts that all code uses)
- ❌ Database schema changes (migrations affect all code)
- ❌ Dependency updates (everyone uses same libraries)
For these cases:
- Serialize the work (one PR handles it)
- OR use feature flags/versioning
- OR accept conflicts and plan for resolution
Scaling: How Many Parallel PRs?
Our experiment: 5 parallel PRs
Could we do more?
| PR Count | Conflict Probability | Coordination Overhead | Sweet Spot? |
|---|---|---|---|
| 2-3 PRs | Very low | Low | ✅ Yes - easy to manage |
| 4-6 PRs | Low (with good design) | Medium | ✅ Yes - our experiment worked well |
| 7-10 PRs | Medium | High | 🟡 Maybe - depends on isolation |
| 11+ PRs | High | Very high | ❌ Likely not worth it |
Factors:
- Communication overhead scales with N² (every PR might affect every other)
- Integration complexity grows exponentially with PR count
- Human orchestration becomes bottleneck beyond 5-7 PRs
Sweet spot: 4-6 parallel PRs for most projects.
Recommendations for Practitioners
✅ Do This
1. Plan file ownership upfront
- Before starting, map out which PR owns which files
- Visualize with a table (like we did above)
- Get everyone to agree on boundaries
2. Create > Modify
- Strongly prefer creating new files over modifying existing
- Even if it means temporary duplication
- Consolidation in integration PR is cheap
3. Use clear naming conventions
-
test_<feature>_<pr_number>.pymakes ownership obvious - Subdirectories per work stream
- Comments at top: "// OWNED BY PR-2, DO NOT MODIFY"
4. Defer cross-cutting changes
- Import updates → integration PR
- Shared config → integration PR
- Database migrations → separate PR (serialized)
5. Test integration continuously
- Integration PR merges frequently (not just at end)
- Run full test suite after each merge
- Catch API mismatches early
❌ Don't Do This
1. Assume zero conflicts means zero work
- Budget for integration debugging time (5-10 hours for 5 PRs)
- Plan for API verification
- Expect cross-PR issues
2. Over-partition
- Don't create 50 tiny PRs just to avoid conflicts
- Sweet spot is 4-6 meaningful work streams
- Coordination overhead grows with PR count
3. Skip planning
- "We'll figure out ownership as we go" → conflicts
- Plan upfront which files each PR owns
- Communicate boundaries clearly
4. Neglect integration testing
- Zero merge conflicts ≠ code works
- Still need comprehensive integration tests
- Budget time for cross-PR verification
The Alternative: Traditional Merge-Heavy Workflow
For comparison, what would traditional approach look like?
Traditional workflow:
- 5 developers work in parallel
- Each modifies shared files (test_video_moderator.py, conftest.py)
- PR-1 merges → conflict-free ✅
- PR-2 merges → 3 conflicts (vs PR-1)
- PR-3 merges → 5 conflicts (vs PR-1, PR-2)
- PR-4 merges → 7 conflicts (vs PR-1, PR-2, PR-3)
- PR-5 merges → 10 conflicts (vs all others)
- Total: 25 conflicts, ~10 hours resolving
Our approach:
- 5 PRs work in parallel
- Each owns separate files
- PR-1 merges → auto ✅
- PR-2 merges → auto ✅
- PR-3 merges → auto ✅
- PR-4 merges → auto ✅
- PR-5 merges → auto ✅
- Total: 0 conflicts, ~0.5 hours merging (git operations)
Time saved on merging: ~9.5 hours
Time spent on integration debugging: ~5 hours
Net savings: ~4.5 hours (50% reduction)
But more importantly: Parallel work was actually possible. Traditional approach would have serialized after conflicts mounted.
Real-World Examples
Example 1: Microservices Architecture
Scenario: 5 teams building microservices
Zero-conflict approach:
services/
├── user-service/ # Team A owns
├── payment-service/ # Team B owns
├── order-service/ # Team C owns
├── notify-service/ # Team D owns
└── analytics-service # Team E owns
Each team:
- Owns their directory completely
- No modifications to other services
- Shared interfaces versioned (v1, v2 APIs)
Result: True parallel development, zero conflicts
Example 2: Frontend Component Library
Scenario: 5 developers building React components
Zero-conflict approach:
components/
├── Button/ # Dev A
│ ├── Button.tsx
│ └── Button.test.tsx
├── Input/ # Dev B
│ ├── Input.tsx
│ └── Input.test.tsx
├── Modal/ # Dev C
├── Dropdown/ # Dev D
└── Table/ # Dev E
Shared changes:
// Integration PR updates index.ts:
export { Button } from './Button';
export { Input } from './Input';
export { Modal } from './Modal';
// ... (deferred to integration)
Result: 5 components developed in parallel, merged without conflicts
Conclusion
What we proved:
- ✅ File-level ownership eliminates merge conflicts (100% auto-merge)
- ✅ Parallel development is possible with good architecture
- ✅ 75% time savings despite orchestration overhead
What we learned:
- ❌ Zero merge conflicts ≠ zero integration work
- ⚠️ Integration debugging still takes significant time (5 hours)
- ⚠️ Architecture solves conflicts, humans solve correctness
The 80/20 insight:
- 80% of success: File-level ownership (one design decision)
- 20% of effort: Everything else (but the hard work)
Would we do it again? Absolutely! Eliminating merge conflicts removed a huge coordination bottleneck.
Next time we'd improve:
- Better API verification upfront (model introspection)
- Clearer communication protocol (automated status updates)
- Pre-planned integration test strategy
What's Next
In the following articles:
-
Article 5: Communication Protocols for AI Agents That Can't Talk
- How we iterated 4 times to get file-based messaging working
- What worked and what didn't
-
Article 6: The Budget Calculator Paradox
- Flip-flopping 8 times on a simple formula
- Build the calculator first, use it everywhere
Tags: #parallel-development #git #merge-conflicts #architecture #software-engineering #team-collaboration #zero-conflict
This is Part 4 of the Multi-Agent Development Series.
Discussion: How do you handle merge conflicts in your team? Have you tried file-level ownership? What's your sweet spot for parallel PR count? Share in the comments!
Top comments (0)