Skip to content

feat: wire up AI conflict resolution for parallel execution#283

Merged
subsy merged 14 commits intomainfrom
feat/ai-conflict-resolution
Feb 5, 2026
Merged

feat: wire up AI conflict resolution for parallel execution#283
subsy merged 14 commits intomainfrom
feat/ai-conflict-resolution

Conversation

@subsy
Copy link
Owner

@subsy subsy commented Feb 4, 2026

Summary

The conflict resolution infrastructure existed but was never actually connected - setAiResolver() was never called after creating ParallelExecutor. This PR wires up actual AI-powered resolution using the session's configured agent.

  • New module: src/parallel/ai-resolver.ts with fast-path heuristics and LLM resolution
  • Fast-path optimization: Skip AI when one side is empty or both sides identical
  • Remote-compatible: Uses same agent as session (works on any node)
  • Context-aware prompts: Includes task context so AI understands worker's intent
  • Configurable: conflictResolution.enabled, timeoutMs, maxFiles in config

What was broken

// ConflictResolver.resolveFile() always fell through to this:
if (this.aiResolver) {  // ← Always null/undefined!
  // ... AI resolution code never ran
}
return {
  success: false,
  error: 'AI resolution failed or unavailable',  // ← Always returned this
};

How it works now

┌─────────────────────────────────────────────────────────────────┐
│                      run.tsx                                    │
│  parallelExecutor.setAiResolver(createAiResolver(config))       │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│              createAiResolver(config) → AiResolverCallback      │
│                                                                 │
│  1. Fast-path checks (one side empty, identical content)        │
│  2. Spawn agent with conflict resolution prompt                 │
│  3. Parse response and extract content                          │
│  4. Return resolved content or null on failure                  │
└─────────────────────────────────────────────────────────────────┘

Test plan

  • Unit tests for tryFastPathResolution() (20 tests pass)
  • Unit tests for buildConflictPrompt()
  • Unit tests for extractResolvedContent()
  • Full test suite passes (185 tests)
  • UBS scan shows no critical issues
  • Manual test with real merge conflicts in parallel execution

Fixes #275

Summary by CodeRabbit

  • New Features

    • AI-powered conflict resolution during parallel runs with fast-path heuristics, per-file timeouts, and Retry/Skip actions.
  • Configuration

    • New conflictResolution settings (enabled by default) to control AI resolution, timeout and max files; schema validation added.
  • UI

    • Conflict panel enhanced: per-file AI progress, currently resolving file, failure states, scrollable list and keyboard shortcuts (r = Retry, s = Skip).
  • Tests & Docs

    • New tests and updated testing/setup/reset guidance and workspace fixtures for conflict scenarios.

@vercel
Copy link

vercel bot commented Feb 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ralph-tui Ignored Ignored Preview Feb 5, 2026 1:14am

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 4, 2026

Caution

Review failed

The pull request is closed.

Walkthrough

Adds an AI-driven conflict-resolution feature: new AI resolver module (fast-path, prompt, agent invocation, output parsing), config/schema/types, ParallelExecutor wiring and pending-conflict retry/skip, TUI plumbing and keyboard actions, tracker serialization, tests, and testing fixtures/scripts.

Changes

Cohort / File(s) Summary
Design doc
docs/plans/2026-02-04-ai-conflict-resolution-design.md
New design plan describing AI resolver architecture, wiring, config, events and tests.
Config types & schema
src/config/types.ts, src/config/schema.ts
Adds ConflictResolutionConfig and optional conflictResolution in StoredConfig/RalphConfig; validation schema (enabled, timeoutMs, maxFiles).
AI resolver implementation & tests
src/parallel/ai-resolver.ts, src/parallel/ai-resolver.test.ts
New factory createAiResolver(config) plus exports tryFastPathResolution, buildConflictPrompt, extractResolvedContent; per-file timeout, fast-path heuristics, agent invocation and parsing; comprehensive unit tests.
Parallel executor & conflict control
src/parallel/index.ts, src/parallel/conflict-resolver.ts
Adds pending-conflict tracking, retryConflictResolution(), skipFailedConflict(), hasPendingConflict(); changes merge flow to collect then resolve conflicts; emits conflict:detected; exports MergeOperation.
Runtime wiring (run command)
src/commands/run.tsx
Wires AI resolver via ParallelExecutor.setAiResolver(createAiResolver(...)) when enabled; adds/forwards props/callbacks (parallelCurrentlyResolvingFile, onConflictRetry, onConflictSkip) into the TUI/run flow.
TUI and keyboard controls
src/tui/components/ConflictResolutionPanel.tsx, src/tui/components/RunApp.tsx
Panel accepts currentlyResolvingFile, onRetry, onSkip; per-file status UI, spinner, scroll, failure actions; keyboard shortcuts r (retry) and s (skip); panel auto-show gated by phase/config.
Tracker plugin serialization
src/plugins/trackers/builtin/json/index.ts, src/plugins/trackers/types.ts
Adds withWriteLock to serialize JSON tracker read-modify-write operations, getStateFiles and clearCache helpers; getStateFiles?() added to TrackerPlugin type.
Engine loop minor change
src/engine/index.ts
Avoids calling tracker.completeTask for forced/worker tasks to prevent duplicate completion when ParallelExecutor handles completion.
Testing fixtures & scripts
testing/test-conflict-prd.json, testing/reset-test.sh, testing/setup-test-workspace.sh, testing/TESTING.md
Adds conflict PRD test and updates setup/reset scripts and documentation to seed/clean conflict PRD outputs and describe conflict-resolution tests.
Package bump
package.json
Version bumped 0.7.0 → 0.7.1.

Sequence Diagram(s)

sequenceDiagram
    participant UI as UI / run.tsx
    participant PE as ParallelExecutor
    participant CR as ConflictResolver
    participant AR as AI Resolver
    participant Agent as Agent Process

    UI->>PE: setAiResolver(createAiResolver(config))
    PE->>PE: store AI resolver callback

    rect rgba(100,150,200,0.5)
    Note over PE,CR: ParallelExecutor detects merge conflict
    PE->>CR: report conflict (file, base, ours, theirs, task ctx)
    end

    rect rgba(150,200,100,0.5)
    CR->>AR: request resolution(conflict, context)
    AR->>AR: tryFastPathResolution(conflict)
    alt Fast-path applies
        AR-->>CR: resolved content (strategy: fast-path)
    else Fast-path fails
        AR->>AR: buildConflictPrompt(conflict, ctx)
        AR->>Agent: spawn agent with prompt + timeout
        Agent->>Agent: LLM processes prompt
        Agent-->>AR: stdout / exit status
        AR->>AR: extractResolvedContent(stdout)
        AR-->>CR: resolved content or null (strategy: ai)
    end
    end

    rect rgba(200,150,100,0.5)
    CR->>PE: apply resolution / emit events / continue execution
    PE->>UI: emit conflict:ai-resolving / ai-resolved / ai-failed events (include strategy)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive All changes directly support AI conflict resolution and keyboard handling improvements. However, changes to JsonTrackerPlugin's write-locking mechanism appear tangentially related rather than core to the stated objectives. Clarify whether write-locking in JsonTrackerPlugin is essential for AI conflict resolution or if it should be in a separate PR for maintenance/stability improvements.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarises the main change: wiring up AI conflict resolution functionality for parallel execution, which is the primary focus across multiple files.
Linked Issues check ✅ Passed The PR addresses issue #275's keyboard/selection problems in the Merge/Conflict pane by redesigning the ConflictResolutionPanel with proper state management, animated UI, and callback wiring for retry/skip actions, restoring keyboard interaction.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/ai-conflict-resolution

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Feb 4, 2026

Codecov Report

❌ Patch coverage is 29.74684% with 333 lines in your changes missing coverage. Please review.
✅ Project coverage is 42.98%. Comparing base (c29ba43) to head (7697ae1).
⚠️ Report is 16 commits behind head on main.

Files with missing lines Patch % Lines
src/parallel/index.ts 3.95% 170 Missing ⚠️
src/commands/run.tsx 1.63% 60 Missing ⚠️
src/parallel/ai-resolver.ts 44.79% 53 Missing ⚠️
src/plugins/trackers/builtin/json/index.ts 61.90% 40 Missing ⚠️
src/parallel/conflict-resolver.ts 72.72% 6 Missing ⚠️
src/engine/index.ts 42.85% 4 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #283      +/-   ##
==========================================
- Coverage   43.20%   42.98%   -0.23%     
==========================================
  Files          94       95       +1     
  Lines       29607    29974     +367     
==========================================
+ Hits        12792    12883      +91     
- Misses      16815    17091     +276     
Files with missing lines Coverage Δ
src/config/schema.ts 95.38% <100.00%> (+0.22%) ⬆️
src/config/types.ts 100.00% <ø> (ø)
src/engine/index.ts 48.50% <42.85%> (-0.06%) ⬇️
src/parallel/conflict-resolver.ts 74.66% <72.72%> (-0.16%) ⬇️
src/plugins/trackers/builtin/json/index.ts 59.26% <61.90%> (-1.72%) ⬇️
src/parallel/ai-resolver.ts 44.79% <44.79%> (ø)
src/commands/run.tsx 10.71% <1.63%> (-0.12%) ⬇️
src/parallel/index.ts 16.24% <3.95%> (-4.03%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@subsy subsy force-pushed the feat/ai-conflict-resolution branch from c7dd781 to 59591ff Compare February 4, 2026 19:59
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@docs/plans/2026-02-04-ai-conflict-resolution-design.md`:
- Around line 49-59: The docs declare fields on ConflictResolutionConfig
(notably confidenceThreshold and strategy) that the implementation doesn't
expose; update either the code or the docs: either add the missing optional
properties (confidenceThreshold: number, strategy: string|enum, with defaults
and used by functions like whatever resolves AI decisions) to the implementation
where ConflictResolutionConfig is defined and consumed, or remove/mark them as
TODO/futureWork in the doc and ensure the doc comment lines 49–59 and the other
referenced section (219–230) consistently reflect the actual implemented shape
and defaults; reference the ConflictResolutionConfig type and any AI
resolution/decision logic functions to wire or document these fields.
- Around line 20-41: The fenced diagram block in the docs lacks a language tag
and violates MD040; update the opening fence for that block to include a
language tag (e.g., "text") so the block is ```text ... ```; locate the block
near references like run.tsx,
parallelExecutor.setAiResolver(createAiResolver(config)),
createAiResolver(config) → AiResolverCallback, and
ConflictResolver/resolveFile() and change the fence accordingly.

In `@src/parallel/ai-resolver.test.ts`:
- Line 6: Remove the unused `mock` import from the test module import list:
update the import statement that currently reads "import { describe, expect,
test, mock } from 'bun:test';" to only import the symbols actually used
(`describe`, `expect`, `test`) so the `mock` identifier is no longer imported
and TS6133 is resolved.
🧹 Nitpick comments (2)
src/parallel/ai-resolver.test.ts (1)

93-105: Test name doesn’t match the expected value.
The test asserts an empty string, not null — consider renaming for clarity.

✏️ Suggested rename
-  test('returns null when both sides are empty', () => {
+  test('returns empty string when both sides are empty', () => {
src/parallel/ai-resolver.ts (1)

135-147: Consider handling fenced blocks that aren’t the entire output.
If the agent adds a preamble plus a fenced block, the current regex won’t strip the fence and the preamble will be written into the file. A more defensive extraction would prefer the first fenced block when present.

🛡️ Suggested adjustment
-  const fenceMatch = content.match(/^```[\w]*\n([\s\S]*)\n```$/);
-  if (fenceMatch) {
-    content = fenceMatch[1] ?? content;
-  }
+  const fenceMatch = content.match(/```[\w]*\n([\s\S]*?)\n```/);
+  if (fenceMatch) {
+    const fenced = fenceMatch[1]?.trim() ?? '';
+    return fenced.length > 0 ? fenced : null;
+  }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@testing/TESTING.md`:
- Around line 272-278: The fenced ASCII diagram block containing the
CONFLICT-001 / CONFLICT-002 / CONFLICT-003 lines is missing a language specifier
which triggers markdownlint MD040; update the fenced code block that surrounds
the CONFLICT-001 ─────┐ ... CONFLICT-003 ─────┘ diagram to include a language
tag such as text or plaintext (e.g., change the opening triple backticks to
```text) so the linter recognizes it as non-executable content while preserving
the diagram.
🧹 Nitpick comments (1)
testing/reset-test.sh (1)

48-54: Consider copying conflict PRD during reset even if absent.

The current logic only resets test-conflict-prd.json if it already exists in the workspace. This is intentional per the documentation (manual copy required), but creates an asymmetry: the standard PRD is always copied, whilst the conflict PRD requires prior manual setup.

If the intent is to keep conflict testing opt-in, this is fine. Otherwise, consider mirroring the standard PRD logic to always copy if the source exists:

♻️ Optional: Always copy conflict PRD if source exists
-# Also reset conflict PRD if it exists in workspace
-if [ -f "$TEST_WORKSPACE/test-conflict-prd.json" ]; then
-    if [ -f "$SCRIPT_DIR/test-conflict-prd.json" ]; then
-        cp "$SCRIPT_DIR/test-conflict-prd.json" "$TEST_WORKSPACE/test-conflict-prd.json"
-        echo -e "${GREEN}  Re-copied test-conflict-prd.json from source${NC}"
-    fi
-fi
+# Also reset conflict PRD if source exists
+if [ -f "$SCRIPT_DIR/test-conflict-prd.json" ]; then
+    cp "$SCRIPT_DIR/test-conflict-prd.json" "$TEST_WORKSPACE/test-conflict-prd.json"
+    echo -e "${GREEN}  Re-copied test-conflict-prd.json from source${NC}"
+fi

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/tui/components/ConflictResolutionPanel.tsx`:
- Around line 171-180: The panel shows AI-specific copy regardless of whether AI
conflict resolution is enabled; add a boolean prop aiEnabled to
ConflictResolutionPanelProps and use it inside the ConflictResolutionPanel
component to conditionally render the "AI resolution runs automatically. Merge
completes when all files resolve." text only when aiEnabled is true; update all
callers (notably the places that render ConflictResolutionPanel in RunApp.tsx)
to pass the config flag conflictResolution.enabled into the new aiEnabled prop
so behavior matches configuration.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/tui/components/ConflictResolutionPanel.tsx`:
- Around line 31-71: When handling the parallel event "conflict:ai-failed"
update the component state to record a failed ConflictResolutionResult for the
target file (instead of only flipping aiResolving) so
getResolutionState(conflicts, resolutions, aiResolving) will detect the failure;
specifically, in the parallel event handler that currently sets aiResolving,
also add or update an entry in the resolutions array with { filePath: <the
conflict filePath>, success: false, /* optional error/message field */ } (or set
an explicit failure flag tied to that file and thread it into
getResolutionState), then set aiResolving = false so the UI shows the retry/skip
controls.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/commands/run.tsx`:
- Around line 917-918: The comment for the property
parallelCurrentlyResolvingFile contains temporal wording—update it (and the
similar comments around the other occurrence) to an evergreen phrasing without
temporal qualifiers; locate the property parallelCurrentlyResolvingFile and
replace its comment "The file currently being resolved by AI" (and the matching
comment near the other occurrence) with a neutral form such as "The file being
resolved by AI" or "File being resolved by AI".

In `@src/tui/components/ConflictResolutionPanel.tsx`:
- Around line 35-36: Update the JSDoc comment for the currentlyResolvingFile
property in ConflictResolutionPanel.tsx to remove temporal language; replace
"The file currently being resolved by AI" with a timeless phrase such as "The
file being resolved by AI" (or similar) so the comment is evergreen while still
describing the currentlyResolvingFile field.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/parallel/index.ts`:
- Around line 512-532: The success branch currently always clears
this.pendingConflictOperation and this.pendingConflictWorkerResult, which drops
earlier failed conflicts; instead only clear those fields if they are null or
refer to the same conflict being completed. Update the allResolved path in the
loop so that before setting this.pendingConflictOperation = null and
this.pendingConflictWorkerResult = null you check whether the existing
pendingConflictWorkerResult is either null or has the same identifier as the
current workerResult (e.g., compare workerResult.task.id); only clear them in
that case, otherwise leave the existing pending failure alone so it can be
retried/skipped later. Ensure the rest of the success logic (await
this.tracker.completeTask(...), await this.mergeProgressFile(workerResult),
totals increment) remains unchanged.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/tui/components/RunApp.tsx (1)

1497-1525: ⚠️ Potential issue | 🔴 Critical

Failed AI resolutions are not tracked in the state checked by retry/skip shortcuts.

The conflict:ai-failed event handler does not append a failed result to parallelState.conflictResolutions, unlike the conflict:ai-resolved handler. This means hasFailures will not detect failures from the AI resolution path since it only checks items in parallelConflictResolutions. The resolver returns a ConflictResolutionResult with success: false on failure, but this result never reaches the array the UI evaluates.

Append the failed result to conflictResolutions when conflict:ai-failed is emitted, or track failures through an explicit state flag.

Also, reword the inline comment "has failed resolutions and not currently resolving" to avoid temporal language — use "has failed resolutions and resolution is not in progress" or similar evergreen phrasing.

🤖 Fix all issues with AI agents
In `@src/commands/run.tsx`:
- Around line 1837-1862: onConflictRetry currently sets
parallelState.aiResolving = true and awaits
parallelExecutor.retryConflictResolution() without error handling, which can
leave aiResolving true on exceptions; wrap the await in a try/catch/finally
inside the onConflictRetry handler (referencing onConflictRetry,
parallelState.aiResolving, parallelExecutor.retryConflictResolution, and
triggerRerender) so that failures are caught (log or surface the error
appropriately) and the finally block always resets parallelState.aiResolving =
false and calls triggerRerender?.() to ensure the UI is not left stuck; also
ensure the handler returns a rejected or resolved Promise consistently so
callers that don't await it don't produce unhandled rejections.

In `@src/tui/components/RunApp.tsx`:
- Around line 179-183: Update the temporal comments to state-based descriptions:
replace "The file currently being resolved by AI" with a neutral, state-focused
phrase for parallelCurrentlyResolvingFile (e.g., "The file being resolved by AI
in parallel mode"), change "Whether to show the conflict panel (true during
Phase 2 conflict resolution)" to a state-based comment for parallelShowConflicts
(e.g., "Whether the conflict panel is shown for conflict resolution"), and
similarly revise any other temporal comments around the parallel-mode logic
(including the occurrences noted near the parallel mode code block) so all
comments describe state rather than time-based conditions.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/parallel/index.ts`:
- Around line 157-183: In retryConflictResolution, preserve the tracker's
in-memory/persistent state before re-running resolution to avoid stale state
(prd.json) overwriting main state: capture the current tracker state (e.g., via
a snapshot/getState method or serialization), then run the
resolution/completeTask/mergeProgressFile steps, and in a finally block restore
the saved tracker state (e.g., setState/restore) so the tracker is returned to
its prior state if the retry mutates it; update retryConflictResolution
(referencing pendingConflictOperation, pendingConflictWorkerResult,
this.tracker.completeTask, and this.mergeProgressFile) to perform save → attempt
resolution/complete/merge → restore.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/parallel/index.ts`:
- Around line 472-480: Wrap the tracker save/restore around merge and conflict
resolution in try/finally to guarantee restore on errors: when you call
this.saveTrackerState() before this.mergeEngine.enqueue(result) /
this.mergeEngine.processNext(), immediately follow with a try { await
this.mergeEngine.processNext(); } finally { await
this.restoreTrackerState(savedState); } and do the same pattern for the block
that calls this.saveTrackerState() before conflictResolver.resolveConflicts()
(ensure you reference saveTrackerState, restoreTrackerState,
mergeEngine.processNext, and conflictResolver.resolveConflicts so
restoreTrackerState(savedState) always runs even if processing throws).

AI Agent added 12 commits February 5, 2026 01:04
The conflict resolution infrastructure existed but was never connected.
This commit wires up actual AI-powered resolution using the session's
configured agent.

Key changes:
- New ai-resolver.ts module with fast-path heuristics and LLM resolution
- Fast-path: skip AI when one side is empty or both sides identical
- Uses same agent as session (works on remote nodes)
- Prompt includes task context so AI understands worker's intent
- Config options: enabled, timeoutMs, maxFiles in conflictResolution

Fixes #275
Add test-conflict-prd.json specifically designed to trigger merge
conflicts during parallel execution. Three parallel tasks all modify
the same FEATURES.md file, forcing AI conflict resolution.

Update TESTING.md with instructions for testing conflict resolution
and update reset-test.sh to clean up conflict test outputs.
…mmands

- setup-test-workspace.sh now copies test-conflict-prd.json
- Both scripts now display commands for running either test
- Updated workspace README to document both test scenarios
The panel was displaying shortcuts (a, r, A) that were never implemented
in the keyboard handler. Updated to show only the working controls:
- j/↓ and k/↑ for navigation
- Esc to close panel

Added explanatory text clarifying that AI resolution runs automatically.
When AI conflict resolution fails, users now have actionable options:
- Press 'r' to retry AI resolution
- Press 's' to skip this task's merge and continue
- Press 'Esc' to close the panel

Added ParallelExecutor methods:
- retryConflictResolution() - re-attempts AI resolution
- skipFailedConflict() - abandons the failed merge
- hasPendingConflict() - checks for pending conflicts

Updated ConflictResolutionPanel to show different controls based on state:
- In-progress: shows navigation only
- Has failures: shows retry/skip/abort options with explanatory text
When multiple workers run in parallel, their ExecutionEngines share the
same tracker instance. Each engine was calling completeTask() when its
agent completed, causing concurrent writes to the PRD file that would
overwrite each other.

This fix skips the tracker update in worker mode (forcedTask set).
The ParallelExecutor is now the sole caller of completeTask() after
a successful merge, ensuring sequential and consistent PRD updates.

Bug: tasks showing passes: false even though work was merged
Root cause: concurrent completeTask calls from parallel workers
Fix: defer tracker update to ParallelExecutor after merge

Fixes #275
1. Add write lock to JSON tracker to prevent race conditions
   - Multiple completeTask calls no longer overwrite each other
   - Uses promise-based mutex to serialize read-modify-write operations
   - Invalidates cache before each write to ensure fresh reads

2. Improve conflict resolution panel UX
   - Add animated spinner (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏) when AI is resolving
   - Show which file is currently being resolved
   - Fixed-height file list with scroll indicators (max 10 visible)
   - Per-file status: resolved, resolving, pending, failed
   - Progress display: "2/5 resolved · 1 pending"

3. Track currently resolving file
   - New `currentlyResolvingFile` state in parallel executor
   - Events track which file AI is working on

Fixes #275
- Refactored ParallelExecutor to use two-phase merge strategy:
  Phase 1: Attempt all merges, collect conflicts
  Phase 2: Resolve all collected conflicts after merges complete
- Simplified conflict resolution UI animation:
  - Removed spinner from header (now static indicator)
  - Animation only appears next to file being resolved
- This fixes the issue where conflict resolution panel appeared
  mid-batch instead of at the end
- Added parallelShowConflicts flag to control panel visibility
- Set showConflicts=true only when conflict:ai-resolving event fires (Phase 2)
- Set showConflicts=false when conflict:resolved event fires
- Updated useEffect to require both parallelShowConflicts and conflicts
- Panel no longer appears prematurely during Phase 1 merge detection
Three interconnected bugs in parallel conflict resolution:

1. PRD file overwritten by stale worktree copies during merge
   - Added getStateFiles() to TrackerPlugin interface
   - ParallelExecutor saves/restores tracker state around merges
   - JSON tracker implements getStateFiles() and clearCache()

2. Wrong conflict list shown in Phase 2 resolution
   - ConflictResolver now emits conflict:detected at start of resolution
   - Ensures UI shows correct files for each task being resolved

3. UI state not updated after successful conflict resolution
   - conflict:resolved handler now moves task to mergedTaskIds
   - Operation status updated from 'conflicted' to 'completed'
   - Merge queue refreshed to reflect new status
1. Add tracker state save/restore to retryConflictResolution()
   - Prevents stale worktree state from overwriting tracker data during retry

2. Add try/catch/finally to onConflictRetry handler
   - Ensures aiResolving is always reset even on exceptions
   - Removes redundant manual state clearing (handled by events)

3. Only clear pendingConflictOperation if it refers to same conflict
   - Prevents dropping earlier failed conflicts when later ones succeed

4. Show keyboard shortcuts only when handlers provided
   - ConflictResolutionPanel now conditionally renders r/s shortcuts

5. Fix docs (MD040 lint, mark unimplemented features as TODO)
   - Added language tag to diagram code block
   - Marked confidenceThreshold and strategy as future work
@subsy subsy force-pushed the feat/ai-conflict-resolution branch from 5a5969a to 1d9fd12 Compare February 5, 2026 01:09
AI Agent added 2 commits February 5, 2026 01:10
Ensures restoreTrackerState() always runs even if mergeEngine.processNext()
or conflictResolver.resolveConflicts() throws an error.
@subsy subsy merged commit 407cd38 into main Feb 5, 2026
6 checks passed
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/tui/components/RunApp.tsx (1)

1497-1527: ⚠️ Potential issue | 🟠 Major

handleKeyboard dependency list is missing new conflict-resolution inputs.

handleKeyboard reads parallelAiResolving, parallelConflictResolutions, onConflictRetry, and onConflictSkip, but they are not in the dependency array. This creates stale closures that prevent the conflict resolution handlers ('r' and 's' keys) from responding correctly.

Suggested fix
-    [displayedTasks, selectedIndex, status, engine, onQuit, viewMode, iterations, iterationSelectedIndex, iterationHistoryLength, onIterationDrillDown, showInterruptDialog, onInterruptConfirm, onInterruptCancel, showHelp, showSettings, showQuitDialog, showKillDialog, showEpicLoader, showRemoteManagement, onStart, storedConfig, onSaveSettings, onLoadEpics, subagentDetailLevel, onSubagentPanelVisibilityChange, currentIteration, maxIterations, renderer, detailsViewMode, subagentPanelVisible, focusedPane, navigateSubagentTree, instanceTabs, selectedTabIndex, onSelectTab, isViewingRemote, displayStatus, instanceManager, isParallelMode, parallelWorkers, parallelConflicts, showConflictPanel, onParallelKill, onParallelPause, onParallelResume, onParallelStart, parallelDerivedStatus]
+    [displayedTasks, selectedIndex, status, engine, onQuit, viewMode, iterations, iterationSelectedIndex, iterationHistoryLength, onIterationDrillDown, showInterruptDialog, onInterruptConfirm, onInterruptCancel, showHelp, showSettings, showQuitDialog, showKillDialog, showEpicLoader, showRemoteManagement, onStart, storedConfig, onSaveSettings, onLoadEpics, subagentDetailLevel, onSubagentPanelVisibilityChange, currentIteration, maxIterations, renderer, detailsViewMode, subagentPanelVisible, focusedPane, navigateSubagentTree, instanceTabs, selectedTabIndex, onSelectTab, isViewingRemote, displayStatus, instanceManager, isParallelMode, parallelWorkers, parallelConflicts, parallelConflictResolutions, parallelAiResolving, showConflictPanel, onConflictRetry, onConflictSkip, onParallelKill, onParallelPause, onParallelResume, onParallelStart, parallelDerivedStatus]
🤖 Fix all issues with AI agents
In `@src/commands/run.tsx`:
- Line 1571: Update the inline comment for the showConflicts property (near
showConflicts: false in src/commands/run.tsx) to remove time‑bound phrasing and
instead describe its behavior and intent; e.g., state that it controls whether
unresolved conflicts are shown (true = show unresolved conflicts, false = hide
when there are none or after they are resolved) so the comment is evergreen and
describes state rather than timing.
- Around line 983-984: The comment for the property parallelShowConflicts
contains temporal wording ("during Phase 2"); update it to be evergreen by
removing the time-bound phrase and describing the intent instead — e.g., change
the JSDoc for parallelShowConflicts to: "Whether to show the conflict panel
(true while resolving conflicts)" or "Whether to show the conflict panel for
conflict resolution" so it documents behavior without referencing phases or
time.

In `@src/parallel/index.ts`:
- Around line 165-195: The finally block calls restoreTrackerState(savedState)
while completion actions (tracker.completeTask, mergeProgressFile, clearing
pendingConflictOperation/pendingConflictWorkerResult, and incrementing
totalConflictsResolved/totalMergesCompleted) run before it, which can let
restoreTrackerState overwrite the completion; change the flow in the method
containing saveTrackerState/restoreTrackerState and
conflictResolver.resolveConflicts so that restoreTrackerState(savedState) is
always called in the finally and all post-success completion work (clear
pendingConflictOperation and pendingConflictWorkerResult, await
tracker.completeTask(workerResult.task.id) with try/catch, await
mergeProgressFile(workerResult), and increment totalConflictsResolved and
totalMergesCompleted) runs after the finally (i.e., after restore) only when
allResolved is true, following the Phase 2 resolution pattern used elsewhere.
🧹 Nitpick comments (2)
testing/TESTING.md (1)

249-249: Consider rewording to avoid split infinitive.

The phrase "to intelligently merge" contains a split infinitive. Whilst this is acceptable in modern English, you may prefer "to merge the content intelligently" for a more traditional style.

✍️ Alternative wording
-4. The AI resolver is invoked to intelligently merge the content
+4. The AI resolver is invoked to merge the content intelligently
src/parallel/ai-resolver.test.ts (1)

93-105: Test name mismatches expected behaviour.
The test says it returns null, but the assertion expects an empty string. Consider renaming for clarity.

💡 Suggested fix
-  test('returns null when both sides are empty', () => {
+  test('returns empty string when both sides are empty', () => {

Comment on lines +983 to +984
/** Whether to show the conflict panel (true during Phase 2 conflict resolution) */
parallelShowConflicts?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove temporal wording from the conflict panel comment.

“During Phase 2” is time‑bound; keep the comment evergreen.
As per coding guidelines, avoid temporal context in comments; make them evergreen.

Proposed change
-  /** Whether to show the conflict panel (true during Phase 2 conflict resolution) */
+  /** Whether to show the conflict panel */
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** Whether to show the conflict panel (true during Phase 2 conflict resolution) */
parallelShowConflicts?: boolean;
/** Whether to show the conflict panel */
parallelShowConflicts?: boolean;
🤖 Prompt for AI Agents
In `@src/commands/run.tsx` around lines 983 - 984, The comment for the property
parallelShowConflicts contains temporal wording ("during Phase 2"); update it to
be evergreen by removing the time-bound phrase and describing the intent instead
— e.g., change the JSDoc for parallelShowConflicts to: "Whether to show the
conflict panel (true while resolving conflicts)" or "Whether to show the
conflict panel for conflict resolution" so it documents behavior without
referencing phases or time.

/** The file currently being resolved by AI */
currentlyResolvingFile: '' as string,
/** Whether to show the conflict panel (set true at Phase 2 start, false when resolved) */
showConflicts: false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid time‑bound phrasing in the showConflicts comment.

“Set true at Phase 2 start, false when resolved” is temporal; keep this evergreen.
As per coding guidelines, avoid temporal context in comments; make them evergreen.

Proposed change
-    /** Whether to show the conflict panel (set true at Phase 2 start, false when resolved) */
+    /** Whether to show the conflict panel */
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
showConflicts: false,
/** Whether to show the conflict panel */
showConflicts: false,
🤖 Prompt for AI Agents
In `@src/commands/run.tsx` at line 1571, Update the inline comment for the
showConflicts property (near showConflicts: false in src/commands/run.tsx) to
remove time‑bound phrasing and instead describe its behavior and intent; e.g.,
state that it controls whether unresolved conflicts are shown (true = show
unresolved conflicts, false = hide when there are none or after they are
resolved) so the comment is evergreen and describes state rather than timing.

Comment on lines +165 to +195
// Save tracker state before resolution to prevent stale worktree state from overwriting
const savedState = await this.saveTrackerState();

try {
// Re-attempt resolution
const resolutions = await this.conflictResolver.resolveConflicts(operation);
const allResolved = resolutions.every((r) => r.success);

if (allResolved) {
// Success! Clear pending state and mark task as complete
this.pendingConflictOperation = null;
this.pendingConflictWorkerResult = null;

try {
await this.tracker.completeTask(workerResult.task.id);
} catch {
// Log but don't fail after successful resolution
}

await this.mergeProgressFile(workerResult);
this.totalConflictsResolved += resolutions.length;
this.totalMergesCompleted++;
return true;
}

// Still failed - keep pending state for another retry
return false;
} finally {
// Always restore tracker state to prevent stale worktree data from persisting
await this.restoreTrackerState(savedState);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Tracker state restore runs after completion, which can undo the completion.
restoreTrackerState() in the finally block can overwrite completeTask() changes on a successful retry. Move completion work after restore (same pattern used in Phase 2 resolution).

🛠️ Suggested fix
-    try {
-      // Re-attempt resolution
-      const resolutions = await this.conflictResolver.resolveConflicts(operation);
-      const allResolved = resolutions.every((r) => r.success);
-
-      if (allResolved) {
-        // Success! Clear pending state and mark task as complete
-        this.pendingConflictOperation = null;
-        this.pendingConflictWorkerResult = null;
-
-        try {
-          await this.tracker.completeTask(workerResult.task.id);
-        } catch {
-          // Log but don't fail after successful resolution
-        }
-
-        await this.mergeProgressFile(workerResult);
-        this.totalConflictsResolved += resolutions.length;
-        this.totalMergesCompleted++;
-        return true;
-      }
-
-      // Still failed - keep pending state for another retry
-      return false;
-    } finally {
-      // Always restore tracker state to prevent stale worktree data from persisting
-      await this.restoreTrackerState(savedState);
-    }
+    let resolutions: Awaited<ReturnType<typeof this.conflictResolver.resolveConflicts>> = [];
+    let allResolved = false;
+    try {
+      // Re-attempt resolution
+      resolutions = await this.conflictResolver.resolveConflicts(operation);
+      allResolved = resolutions.every((r) => r.success);
+    } finally {
+      // Always restore tracker state to prevent stale worktree data from persisting
+      await this.restoreTrackerState(savedState);
+    }
+
+    if (allResolved) {
+      // Success! Clear pending state and mark task as complete
+      this.pendingConflictOperation = null;
+      this.pendingConflictWorkerResult = null;
+
+      try {
+        await this.tracker.completeTask(workerResult.task.id);
+      } catch {
+        // Log but don't fail after successful resolution
+      }
+
+      await this.mergeProgressFile(workerResult);
+      this.totalConflictsResolved += resolutions.length;
+      this.totalMergesCompleted++;
+      return true;
+    }
+
+    // Still failed - keep pending state for another retry
+    return false;
🤖 Prompt for AI Agents
In `@src/parallel/index.ts` around lines 165 - 195, The finally block calls
restoreTrackerState(savedState) while completion actions (tracker.completeTask,
mergeProgressFile, clearing
pendingConflictOperation/pendingConflictWorkerResult, and incrementing
totalConflictsResolved/totalMergesCompleted) run before it, which can let
restoreTrackerState overwrite the completion; change the flow in the method
containing saveTrackerState/restoreTrackerState and
conflictResolver.resolveConflicts so that restoreTrackerState(savedState) is
always called in the finally and all post-success completion work (clear
pendingConflictOperation and pendingConflictWorkerResult, await
tracker.completeTask(workerResult.task.id) with try/catch, await
mergeProgressFile(workerResult), and increment totalConflictsResolved and
totalMergesCompleted) runs after the finally (i.e., after restore) only when
allResolved is true, following the Phase 2 resolution pattern used elsewhere.

sakaman pushed a commit to sakaman/ralph-tui that referenced this pull request Feb 15, 2026
feat: wire up AI conflict resolution for parallel execution
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Keyboard/Selection is not working in Merge/Conflict pane

1 participant

Comments