/** * @fileoverview TaskExecutionService for handling task execution business logic * Extracted from CLI start command to be reusable across CLI and extension */ import type { Task } from '../types/index.js'; import type { TaskService } from './task-service.js'; export interface StartTaskOptions { subtaskId?: string; dryRun?: boolean; updateStatus?: boolean; force?: boolean; silent?: boolean; } export interface StartTaskResult { task: Task | null; found: boolean; started: boolean; subtaskId?: string; subtask?: any; error?: string; executionOutput?: string; /** Command to execute (for CLI to run directly) */ command?: { executable: string; args: string[]; cwd: string; }; } export interface ConflictCheckResult { canProceed: boolean; conflictingTasks: Task[]; reason?: string; } /** * TaskExecutionService handles the business logic for starting and executing tasks */ export class TaskExecutionService { constructor(private taskService: TaskService) {} /** * Start working on a task with comprehensive business logic */ async startTask( taskId: string, options: StartTaskOptions = {} ): Promise { try { // Handle subtask IDs by extracting parent task ID const { parentId, subtaskId } = this.parseTaskId(taskId); // Check for in-progress task conflicts if (!options.force) { const conflictCheck = await this.checkInProgressConflicts(taskId); if (!conflictCheck.canProceed) { return { task: null, found: false, started: false, error: `Conflicting tasks in progress: ${conflictCheck.conflictingTasks.map((t) => `#${t.id}: ${t.title}`).join(', ')}` }; } } // Get the actual task (parent task if dealing with subtask) const task = await this.taskService.getTask(parentId); if (!task) { return { task: null, found: false, started: false, error: `Task ${parentId} not found` }; } // Find the specific subtask if provided let subtask = undefined; if (subtaskId && task.subtasks) { subtask = task.subtasks.find((st) => String(st.id) === subtaskId); } // Update task status to in-progress if not disabled if (options.updateStatus && !options.dryRun) { try { await this.taskService.updateTaskStatus(parentId, 'in-progress'); } catch (error) { // Log but don't fail - status update is not critical console.warn( `Could not update task status: ${error instanceof Error ? error.message : String(error)}` ); } } // Prepare execution command instead of executing directly let started = false; let executionOutput = 'Task ready to execute'; let command = undefined; if (!options.dryRun) { // Prepare the command for execution by the CLI command = await this.prepareExecutionCommand(task, subtask); started = !!command; // Command prepared successfully executionOutput = command ? `Command prepared: ${command.executable} ${command.args.join(' ')}` : 'Failed to prepare execution command'; } else { // For dry-run, just show that we would execute started = true; executionOutput = 'Dry run - task would be executed'; // Also prepare command for dry run display command = await this.prepareExecutionCommand(task, subtask); } return { task, found: true, started, subtaskId, subtask, executionOutput, command: command || undefined }; } catch (error) { return { task: null, found: false, started: false, error: error instanceof Error ? error.message : String(error) }; } } /** * Check for existing in-progress tasks and determine conflicts */ async checkInProgressConflicts( targetTaskId: string ): Promise { const allTasks = await this.taskService.getTaskList(); const inProgressTasks = allTasks.tasks.filter( (task) => task.status === 'in-progress' ); // If the target task is already in-progress, that's fine const targetTaskInProgress = inProgressTasks.find( (task) => task.id === targetTaskId ); if (targetTaskInProgress) { return { canProceed: true, conflictingTasks: [] }; } // Check if target is a subtask and its parent is in-progress const isSubtask = targetTaskId.includes('.'); if (isSubtask) { const parentTaskId = targetTaskId.split('.')[0]; const parentInProgress = inProgressTasks.find( (task) => task.id === parentTaskId ); if (parentInProgress) { return { canProceed: true, conflictingTasks: [] }; // Allow subtasks when parent is in-progress } } // Check if other unrelated tasks are in-progress const conflictingTasks = inProgressTasks.filter((task) => { if (task.id === targetTaskId) return false; // If target is a subtask, exclude its parent from conflicts if (isSubtask) { const parentTaskId = targetTaskId.split('.')[0]; if (task.id === parentTaskId) return false; } // If the in-progress task is a subtask of our target parent, exclude it if (task.id.toString().includes('.')) { const taskParentId = task.id.toString().split('.')[0]; if (isSubtask && taskParentId === targetTaskId.split('.')[0]) { return false; } } return true; }); if (conflictingTasks.length > 0) { return { canProceed: false, conflictingTasks, reason: 'Other tasks are already in progress' }; } return { canProceed: true, conflictingTasks: [] }; } /** * Get the next available task to start */ async getNextAvailableTask(): Promise { const nextTask = await this.taskService.getNextTask(); return nextTask?.id || null; } /** * Parse a task ID to determine if it's a subtask and extract components */ private parseTaskId(taskId: string): { parentId: string; subtaskId?: string; } { if (taskId.includes('.')) { const [parentId, subtaskId] = taskId.split('.'); return { parentId, subtaskId }; } return { parentId: taskId }; } /** * Check if a task can be started (no conflicts) */ async canStartTask(taskId: string, force = false): Promise { if (force) return true; const conflictCheck = await this.checkInProgressConflicts(taskId); return conflictCheck.canProceed; } /** * Prepare execution command for the CLI to run */ private async prepareExecutionCommand( task: Task, subtask?: any ): Promise<{ executable: string; args: string[]; cwd: string } | null> { try { // Format the task into a prompt const taskPrompt = this.formatTaskPrompt(task, subtask); // Use claude command - could be extended for other executors const executable = 'claude'; const args = [taskPrompt]; const cwd = process.cwd(); // or could get from project root return { executable, args, cwd }; } catch (error) { console.warn( `Failed to prepare execution command: ${error instanceof Error ? error.message : String(error)}` ); return null; } } /** * Format task into a prompt suitable for execution */ private formatTaskPrompt(task: Task, subtask?: any): string { const workItem = subtask || task; const itemType = subtask ? 'Subtask' : 'Task'; const itemId = subtask ? `${task.id}.${subtask.id}` : task.id; let prompt = `${itemType} #${itemId}: ${workItem.title}\n\n`; if (workItem.description) { prompt += `Description:\n${workItem.description}\n\n`; } if (workItem.details) { prompt += `Implementation Details:\n${workItem.details}\n\n`; } if (workItem.testStrategy) { prompt += `Test Strategy:\n${workItem.testStrategy}\n\n`; } if (task.dependencies && task.dependencies.length > 0) { prompt += `Dependencies: ${task.dependencies.join(', ')}\n\n`; } prompt += `Please help me implement this ${itemType.toLowerCase()}.`; return prompt; } /** * Get task with subtask resolution */ async getTaskWithSubtask( taskId: string ): Promise<{ task: Task | null; subtask?: any; subtaskId?: string }> { const { parentId, subtaskId } = this.parseTaskId(taskId); const task = await this.taskService.getTask(parentId); if (!task) { return { task: null }; } if (subtaskId && task.subtasks) { const subtask = task.subtasks.find((st) => String(st.id) === subtaskId); return { task, subtask, subtaskId }; } return { task }; } }