/** * @fileoverview Task Loader Service * Loads and validates tasks for autopilot execution */ import type { Task, Subtask, TaskStatus } from '../types/index.js'; import { TaskService } from './task-service.js'; import { ConfigManager } from '../config/config-manager.js'; import { getLogger } from '../logger/factory.js'; const logger = getLogger('TaskLoader'); /** * Validation error types */ export type ValidationErrorType = | 'task_not_found' | 'task_completed' | 'no_subtasks' | 'circular_dependencies' | 'missing_dependencies' | 'invalid_structure'; /** * Validation result for task loading */ export interface TaskValidationResult { /** Whether validation passed */ success: boolean; /** Loaded task (only present if validation succeeded) */ task?: Task; /** Error type */ errorType?: ValidationErrorType; /** Human-readable error message */ errorMessage?: string; /** Actionable suggestion for fixing the error */ suggestion?: string; /** Dependency analysis (only for dependency errors) */ dependencyIssues?: DependencyIssue[]; } /** * Dependency issue details */ export interface DependencyIssue { /** Subtask ID with the issue */ subtaskId: string; /** Type of dependency issue */ issueType: 'circular' | 'missing' | 'invalid'; /** Description of the issue */ message: string; /** The problematic dependency reference */ dependencyRef?: string; } /** * TaskLoaderService loads and validates tasks for autopilot execution */ export class TaskLoaderService { private taskService: TaskService | null = null; private projectRoot: string; constructor(projectRoot: string) { if (!projectRoot) { throw new Error('projectRoot is required for TaskLoaderService'); } this.projectRoot = projectRoot; } /** * Ensure TaskService is initialized */ private async ensureInitialized(): Promise { if (this.taskService) return; const configManager = await ConfigManager.create(this.projectRoot); this.taskService = new TaskService(configManager); await this.taskService.initialize(); } /** * Load and validate a task for autopilot execution */ async loadAndValidateTask(taskId: string): Promise { logger.info(`Loading task ${taskId}...`); // Step 1: Load task const task = await this.loadTask(taskId); if (!task) { return { success: false, errorType: 'task_not_found', errorMessage: `Task with ID "${taskId}" not found`, suggestion: 'Use "task-master list" to see available tasks or verify the task ID is correct.' }; } // Step 2: Validate task status const statusValidation = this.validateTaskStatus(task); if (!statusValidation.success) { return statusValidation; } // Step 3: Check for subtasks const subtaskValidation = this.validateSubtasksExist(task); if (!subtaskValidation.success) { return subtaskValidation; } // Step 4: Validate subtask structure const structureValidation = this.validateSubtaskStructure(task); if (!structureValidation.success) { return structureValidation; } // Step 5: Analyze dependencies const dependencyValidation = this.validateDependencies(task); if (!dependencyValidation.success) { return dependencyValidation; } logger.info(`Task ${taskId} validated successfully`); return { success: true, task }; } /** * Load task using TaskService */ private async loadTask(taskId: string): Promise { try { await this.ensureInitialized(); if (!this.taskService) { throw new Error('TaskService initialization failed'); } return await this.taskService.getTask(taskId); } catch (error) { logger.error(`Failed to load task ${taskId}:`, error); return null; } } /** * Validate task status is appropriate for autopilot */ private validateTaskStatus(task: Task): TaskValidationResult { const completedStatuses: TaskStatus[] = ['done', 'completed', 'cancelled']; if (completedStatuses.includes(task.status)) { return { success: false, errorType: 'task_completed', errorMessage: `Task "${task.title}" is already ${task.status}`, suggestion: 'Autopilot can only execute tasks that are pending or in-progress. Use a different task.' }; } return { success: true }; } /** * Validate task has subtasks */ private validateSubtasksExist(task: Task): TaskValidationResult { if (!task.subtasks || task.subtasks.length === 0) { return { success: false, errorType: 'no_subtasks', errorMessage: `Task "${task.title}" has no subtasks`, suggestion: this.buildExpansionSuggestion(task) }; } return { success: true }; } /** * Build helpful suggestion for expanding tasks */ private buildExpansionSuggestion(task: Task): string { const suggestions: string[] = [ `Autopilot requires tasks to be broken down into subtasks for execution.` ]; // Add expansion command suggestion suggestions.push(`\nExpand this task using:`); suggestions.push(` task-master expand --id=${task.id}`); // If task has complexity analysis, mention it if (task.complexity || task.recommendedSubtasks) { suggestions.push( `\nThis task has complexity analysis available. Consider reviewing it first:` ); suggestions.push(` task-master show ${task.id}`); } else { suggestions.push( `\nOr analyze task complexity first to determine optimal subtask count:` ); suggestions.push(` task-master analyze-complexity --from=${task.id}`); } return suggestions.join('\n'); } /** * Validate subtask structure */ private validateSubtaskStructure(task: Task): TaskValidationResult { for (const subtask of task.subtasks) { // Check required fields if (!subtask.title || !subtask.description) { return { success: false, errorType: 'invalid_structure', errorMessage: `Subtask ${task.id}.${subtask.id} is missing required fields`, suggestion: 'Subtasks must have title and description. Re-expand the task or manually fix the subtask structure.' }; } // Validate dependencies are arrays if (subtask.dependencies && !Array.isArray(subtask.dependencies)) { return { success: false, errorType: 'invalid_structure', errorMessage: `Subtask ${task.id}.${subtask.id} has invalid dependencies format`, suggestion: 'Dependencies must be an array. Fix the task structure manually.' }; } } return { success: true }; } /** * Validate subtask dependencies */ private validateDependencies(task: Task): TaskValidationResult { const issues: DependencyIssue[] = []; const subtaskIds = new Set(task.subtasks.map((st) => String(st.id))); for (const subtask of task.subtasks) { const subtaskId = `${task.id}.${subtask.id}`; // Check for missing dependencies if (subtask.dependencies && subtask.dependencies.length > 0) { for (const depId of subtask.dependencies) { const depIdStr = String(depId); if (!subtaskIds.has(depIdStr)) { issues.push({ subtaskId, issueType: 'missing', message: `References non-existent subtask ${depIdStr}`, dependencyRef: depIdStr }); } } } // Check for circular dependencies const circularCheck = this.detectCircularDependency( subtask, task.subtasks, new Set() ); if (circularCheck) { issues.push({ subtaskId, issueType: 'circular', message: `Circular dependency detected: ${circularCheck.join(' -> ')}` }); } } if (issues.length > 0) { const errorType = issues[0].issueType === 'circular' ? 'circular_dependencies' : 'missing_dependencies'; return { success: false, errorType, errorMessage: `Task "${task.title}" has dependency issues`, suggestion: 'Fix dependency issues manually or re-expand the task:\n' + issues .map((issue) => ` - ${issue.subtaskId}: ${issue.message}`) .join('\n'), dependencyIssues: issues }; } return { success: true }; } /** * Detect circular dependencies using depth-first search */ private detectCircularDependency( subtask: Subtask, allSubtasks: Subtask[], visited: Set ): string[] | null { const subtaskId = String(subtask.id); if (visited.has(subtaskId)) { return [subtaskId]; } visited.add(subtaskId); if (subtask.dependencies && subtask.dependencies.length > 0) { for (const depId of subtask.dependencies) { const depIdStr = String(depId); const dependency = allSubtasks.find((st) => String(st.id) === depIdStr); if (dependency) { const circular = this.detectCircularDependency( dependency, allSubtasks, new Set(visited) ); if (circular) { return [subtaskId, ...circular]; } } } } return null; } /** * Get ordered subtask execution sequence * Returns subtasks in dependency order (tasks with no deps first) */ getExecutionOrder(task: Task): Subtask[] { const ordered: Subtask[] = []; const completed = new Set(); // Keep adding subtasks whose dependencies are all completed while (ordered.length < task.subtasks.length) { let added = false; for (const subtask of task.subtasks) { const subtaskId = String(subtask.id); if (completed.has(subtaskId)) { continue; } // Check if all dependencies are completed const allDepsCompleted = !subtask.dependencies || subtask.dependencies.length === 0 || subtask.dependencies.every((depId) => completed.has(String(depId))); if (allDepsCompleted) { ordered.push(subtask); completed.add(subtaskId); added = true; break; } } // Safety check to prevent infinite loop if (!added && ordered.length < task.subtasks.length) { logger.warn( `Could not determine complete execution order for task ${task.id}` ); // Add remaining subtasks in original order for (const subtask of task.subtasks) { if (!completed.has(String(subtask.id))) { ordered.push(subtask); } } break; } } return ordered; } /** * Clean up resources */ async cleanup(): Promise { // TaskService doesn't require explicit cleanup // Resources are automatically released when instance is garbage collected } }