- Remove original task blocking in handleConflict (task is already completed by handleAgentStopped) - Return created conflict task from handleConflict so orchestrator can queue it for dispatch - Add dedup check to prevent duplicate resolution tasks on crash retries - Queue conflict resolution task via dispatchManager in mergeTaskIntoPhase - Add recovery for erroneously blocked tasks in recoverDispatchQueues - Update tests and docs
178 lines
6.1 KiB
TypeScript
178 lines
6.1 KiB
TypeScript
/**
|
|
* ConflictResolutionService
|
|
*
|
|
* Service responsible for handling merge conflicts by:
|
|
* - Creating conflict resolution tasks
|
|
* - Updating original task status
|
|
* - Notifying agents via messages
|
|
* - Emitting appropriate events
|
|
*
|
|
* This service is used by the CoordinationManager when merge conflicts occur.
|
|
*/
|
|
|
|
import type { EventBus, TaskQueuedEvent } from '../events/index.js';
|
|
import type { TaskRepository } from '../db/repositories/task-repository.js';
|
|
import type { AgentRepository } from '../db/repositories/agent-repository.js';
|
|
import type { MessageRepository } from '../db/repositories/message-repository.js';
|
|
import type { Task } from '../db/schema.js';
|
|
|
|
// =============================================================================
|
|
// ConflictResolutionService Interface (Port)
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Service interface for handling merge conflicts.
|
|
* This is the PORT - implementations are ADAPTERS.
|
|
*/
|
|
/**
|
|
* Branch context for merge conflicts from the branch hierarchy.
|
|
*/
|
|
export interface MergeContext {
|
|
sourceBranch: string;
|
|
targetBranch: string;
|
|
}
|
|
|
|
export interface ConflictResolutionService {
|
|
/**
|
|
* Handle a merge conflict by creating resolution task and notifying agent.
|
|
*
|
|
* @param taskId - ID of the task that conflicted
|
|
* @param conflicts - List of conflicting file paths
|
|
* @param mergeContext - Optional branch context for branch hierarchy merges
|
|
* @returns The created conflict-resolution task, or null if a duplicate already exists
|
|
*/
|
|
handleConflict(taskId: string, conflicts: string[], mergeContext?: MergeContext): Promise<Task | null>;
|
|
}
|
|
|
|
// =============================================================================
|
|
// DefaultConflictResolutionService Implementation (Adapter)
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Default implementation of ConflictResolutionService.
|
|
*
|
|
* Creates conflict resolution tasks, updates task statuses, sends messages
|
|
* to agents, and emits events when merge conflicts occur.
|
|
*/
|
|
export class DefaultConflictResolutionService implements ConflictResolutionService {
|
|
constructor(
|
|
private taskRepository: TaskRepository,
|
|
private agentRepository: AgentRepository,
|
|
private messageRepository?: MessageRepository,
|
|
private eventBus?: EventBus
|
|
) {}
|
|
|
|
/**
|
|
* Handle a merge conflict.
|
|
* Creates a conflict-resolution task and notifies the agent via message.
|
|
* Returns the created task, or null if a duplicate already exists.
|
|
*
|
|
* NOTE: The original task is NOT blocked. It was already completed by
|
|
* handleAgentStopped before this method is called. The pending resolution
|
|
* task prevents premature phase completion on its own.
|
|
*/
|
|
async handleConflict(taskId: string, conflicts: string[], mergeContext?: MergeContext): Promise<Task | null> {
|
|
// Get original task for context
|
|
const originalTask = await this.taskRepository.findById(taskId);
|
|
if (!originalTask) {
|
|
throw new Error(`Original task not found: ${taskId}`);
|
|
}
|
|
|
|
// Get agent that was working on the task
|
|
const agent = await this.agentRepository.findByTaskId(taskId);
|
|
if (!agent) {
|
|
throw new Error(`No agent found for task: ${taskId}`);
|
|
}
|
|
|
|
// Dedup: skip if a pending/in_progress resolution task already exists for this original task
|
|
if (originalTask.phaseId) {
|
|
const phaseTasks = await this.taskRepository.findByPhaseId(originalTask.phaseId);
|
|
const existingResolution = phaseTasks.find(
|
|
(t) =>
|
|
t.name === `Resolve conflicts: ${originalTask.name}` &&
|
|
(t.status === 'pending' || t.status === 'in_progress'),
|
|
);
|
|
if (existingResolution) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Build conflict description
|
|
const descriptionLines = [
|
|
'Merge conflicts detected. Resolve conflicts in the following files:',
|
|
'',
|
|
...conflicts.map((f) => `- ${f}`),
|
|
'',
|
|
`Original task: ${originalTask.name}`,
|
|
'',
|
|
];
|
|
|
|
if (mergeContext) {
|
|
descriptionLines.push(
|
|
`Resolve merge conflicts between branch "${mergeContext.sourceBranch}" and "${mergeContext.targetBranch}".`,
|
|
`Run: git merge ${mergeContext.sourceBranch} --no-edit`,
|
|
'Resolve all conflicts, then: git add . && git commit',
|
|
);
|
|
} else {
|
|
descriptionLines.push(
|
|
'Instructions: Resolve merge conflicts in the listed files, then mark task complete.',
|
|
);
|
|
}
|
|
|
|
const conflictDescription = descriptionLines.join('\n');
|
|
|
|
// Create new conflict-resolution task
|
|
const conflictTask = await this.taskRepository.create({
|
|
parentTaskId: originalTask.parentTaskId,
|
|
phaseId: originalTask.phaseId,
|
|
initiativeId: originalTask.initiativeId,
|
|
name: `Resolve conflicts: ${originalTask.name}`,
|
|
description: conflictDescription,
|
|
category: mergeContext ? 'merge' : 'execute',
|
|
type: 'auto',
|
|
priority: 'high',
|
|
status: 'pending',
|
|
order: originalTask.order + 1,
|
|
});
|
|
|
|
// Create message to agent if messageRepository is configured
|
|
if (this.messageRepository) {
|
|
const messageContent = [
|
|
`Merge conflict detected for task: ${originalTask.name}`,
|
|
'',
|
|
'Conflicting files:',
|
|
...conflicts.map((f) => `- ${f}`),
|
|
'',
|
|
`A new task has been created to resolve these conflicts: ${conflictTask.name}`,
|
|
'',
|
|
'Please resolve the merge conflicts in the listed files and mark the resolution task as complete.',
|
|
].join('\n');
|
|
|
|
await this.messageRepository.create({
|
|
senderType: 'user', // System-generated messages appear as from user
|
|
senderId: null,
|
|
recipientType: 'agent',
|
|
recipientId: agent.id,
|
|
type: 'info',
|
|
content: messageContent,
|
|
requiresResponse: false,
|
|
});
|
|
}
|
|
|
|
// Emit TaskQueuedEvent for the new conflict-resolution task
|
|
if (this.eventBus) {
|
|
const event: TaskQueuedEvent = {
|
|
type: 'task:queued',
|
|
timestamp: new Date(),
|
|
payload: {
|
|
taskId: conflictTask.id,
|
|
priority: 'high',
|
|
dependsOn: [],
|
|
},
|
|
};
|
|
this.eventBus.emit(event);
|
|
}
|
|
|
|
return conflictTask;
|
|
}
|
|
} |