UNPKG

task-engine-ai-core

Version:

Revolutionary AI-driven task management system with complete transformation trilogy: Frontend v0.1.0, Backend v0.2.0, CLI v0.3.0 - Enterprise-grade performance with 95% improvements

776 lines (688 loc) 26.3 kB
import chalk from 'chalk'; import boxen from 'boxen'; import readline from 'readline'; import fs from 'fs'; import { log, readJSON, writeJSON, isSilentMode } from '../utils.js'; import { startLoadingIndicator, stopLoadingIndicator, displayAiUsageSummary } from '../ui.js'; import { generateTextService } from '../ai-services-unified.js'; import { getDebugFlag, getProjectName } from '../config-manager.js'; import { COMPLEXITY_REPORT_FILE, LEGACY_TASKS_FILE } from '../../../src/constants/paths.js'; /** * Generates the prompt for complexity analysis. * (Moved from ai-services.js and simplified) * @param {Object} tasksData - The tasks data object. * @returns {string} The generated prompt. */ function generateInternalComplexityAnalysisPrompt(tasksData) { const tasksString = JSON.stringify(tasksData.tasks, null, 2); return `Analyze the following tasks to determine their complexity (1-10 scale) and recommend the number of subtasks for expansion. Provide a brief reasoning and an initial expansion prompt for each. Tasks: ${tasksString} Respond ONLY with a valid JSON array matching the schema: [ { "taskId": <number>, "taskTitle": "<string>", "complexityScore": <number 1-10>, "recommendedSubtasks": <number>, "expansionPrompt": "<string>", "reasoning": "<string>" }, ... ] Do not include any explanatory text, markdown formatting, or code block markers before or after the JSON array.`; } /** * Analyzes task complexity and generates expansion recommendations * @param {Object} options Command options * @param {string} options.file - Path to tasks file * @param {string} options.output - Path to report output file * @param {string|number} [options.threshold] - Complexity threshold * @param {boolean} [options.research] - Use research role * @param {string} [options.projectRoot] - Project root path (for MCP/env fallback). * @param {string} [options.id] - Comma-separated list of task IDs to analyze specifically * @param {number} [options.from] - Starting task ID in a range to analyze * @param {number} [options.to] - Ending task ID in a range to analyze * @param {Object} [options._filteredTasksData] - Pre-filtered task data (internal use) * @param {number} [options._originalTaskCount] - Original task count (internal use) * @param {Object} context - Context object, potentially containing session and mcpLog * @param {Object} [context.session] - Session object from MCP server (optional) * @param {Object} [context.mcpLog] - MCP logger object (optional) * @param {function} [context.reportProgress] - Deprecated: Function to report progress (ignored) */ async function analyzeTaskComplexity(options, context = {}) { const { session, mcpLog } = context; const tasksPath = options.file || LEGACY_TASKS_FILE; const outputPath = options.output || COMPLEXITY_REPORT_FILE; const thresholdScore = parseFloat(options.threshold || '5'); const useResearch = options.research || false; const projectRoot = options.projectRoot; // New parameters for task ID filtering const specificIds = options.id ? options.id .split(',') .map((id) => parseInt(id.trim(), 10)) .filter((id) => !Number.isNaN(id)) : null; const fromId = options.from !== undefined ? parseInt(options.from, 10) : null; const toId = options.to !== undefined ? parseInt(options.to, 10) : null; const outputFormat = mcpLog ? 'json' : 'text'; const reportLog = (message, level = 'info') => { if (mcpLog) { mcpLog[level](message); } else if (!isSilentMode() && outputFormat === 'text') { log(level, message); } }; if (outputFormat === 'text') { console.log( chalk.blue( 'Analyzing task complexity and generating expansion recommendations...' ) ); } try { reportLog(`Reading tasks from ${tasksPath}...`, 'info'); let tasksData; let originalTaskCount = 0; let originalData = null; if (options._filteredTasksData) { tasksData = options._filteredTasksData; originalTaskCount = options._originalTaskCount || tasksData.tasks.length; if (!options._originalTaskCount) { try { originalData = readJSON(tasksPath); if (originalData && originalData.tasks) { originalTaskCount = originalData.tasks.length; } } catch (e) { log('warn', `Could not read original tasks file: ${e.message}`); } } } else { originalData = readJSON(tasksPath); if ( !originalData || !originalData.tasks || !Array.isArray(originalData.tasks) || originalData.tasks.length === 0 ) { throw new Error('No tasks found in the tasks file'); } originalTaskCount = originalData.tasks.length; // Filter tasks based on active status const activeStatuses = ['pending', 'blocked', 'in-progress']; let filteredTasks = originalData.tasks.filter((task) => activeStatuses.includes(task.status?.toLowerCase() || 'pending') ); // Apply ID filtering if specified if (specificIds && specificIds.length > 0) { reportLog( `Filtering tasks by specific IDs: ${specificIds.join(', ')}`, 'info' ); filteredTasks = filteredTasks.filter((task) => specificIds.includes(task.id) ); if (outputFormat === 'text') { if (filteredTasks.length === 0 && specificIds.length > 0) { console.log( chalk.yellow( `Warning: No active tasks found with IDs: ${specificIds.join(', ')}` ) ); } else if (filteredTasks.length < specificIds.length) { const foundIds = filteredTasks.map((t) => t.id); const missingIds = specificIds.filter( (id) => !foundIds.includes(id) ); console.log( chalk.yellow( `Warning: Some requested task IDs were not found or are not active: ${missingIds.join(', ')}` ) ); } } } // Apply range filtering if specified else if (fromId !== null || toId !== null) { const effectiveFromId = fromId !== null ? fromId : 1; const effectiveToId = toId !== null ? toId : Math.max(...originalData.tasks.map((t) => t.id)); reportLog( `Filtering tasks by ID range: ${effectiveFromId} to ${effectiveToId}`, 'info' ); filteredTasks = filteredTasks.filter( (task) => task.id >= effectiveFromId && task.id <= effectiveToId ); if (outputFormat === 'text' && filteredTasks.length === 0) { console.log( chalk.yellow( `Warning: No active tasks found in range: ${effectiveFromId}-${effectiveToId}` ) ); } } tasksData = { ...originalData, tasks: filteredTasks, _originalTaskCount: originalTaskCount }; } const skippedCount = originalTaskCount - tasksData.tasks.length; reportLog( `Found ${originalTaskCount} total tasks in the task file.`, 'info' ); // Updated messaging to reflect filtering logic if (specificIds || fromId !== null || toId !== null) { const filterMsg = specificIds ? `Analyzing ${tasksData.tasks.length} tasks with specific IDs: ${specificIds.join(', ')}` : `Analyzing ${tasksData.tasks.length} tasks in range: ${fromId || 1} to ${toId || 'end'}`; reportLog(filterMsg, 'info'); if (outputFormat === 'text') { console.log(chalk.blue(filterMsg)); } } else if (skippedCount > 0) { const skipMessage = `Skipping ${skippedCount} tasks marked as done/cancelled/deferred. Analyzing ${tasksData.tasks.length} active tasks.`; reportLog(skipMessage, 'info'); if (outputFormat === 'text') { console.log(chalk.yellow(skipMessage)); } } // Check for existing report before doing analysis let existingReport = null; let existingAnalysisMap = new Map(); // For quick lookups by task ID try { if (fs.existsSync(outputPath)) { existingReport = readJSON(outputPath); reportLog(`Found existing complexity report at ${outputPath}`, 'info'); if ( existingReport && existingReport.complexityAnalysis && Array.isArray(existingReport.complexityAnalysis) ) { // Create lookup map of existing analysis entries existingReport.complexityAnalysis.forEach((item) => { existingAnalysisMap.set(item.taskId, item); }); reportLog( `Existing report contains ${existingReport.complexityAnalysis.length} task analyses`, 'info' ); } } } catch (readError) { reportLog( `Warning: Could not read existing report: ${readError.message}`, 'warn' ); existingReport = null; existingAnalysisMap.clear(); } if (tasksData.tasks.length === 0) { // If using ID filtering but no matching tasks, return existing report or empty if (existingReport && (specificIds || fromId !== null || toId !== null)) { reportLog( `No matching tasks found for analysis. Keeping existing report.`, 'info' ); if (outputFormat === 'text') { console.log( chalk.yellow( `No matching tasks found for analysis. Keeping existing report.` ) ); } return { report: existingReport, telemetryData: null }; } // Otherwise create empty report const emptyReport = { meta: { generatedAt: new Date().toISOString(), tasksAnalyzed: 0, thresholdScore: thresholdScore, projectName: getProjectName(session), usedResearch: useResearch }, complexityAnalysis: existingReport?.complexityAnalysis || [] }; reportLog(`Writing complexity report to ${outputPath}...`, 'info'); writeJSON(outputPath, emptyReport); reportLog( `Task complexity analysis complete. Report written to ${outputPath}`, 'success' ); if (outputFormat === 'text') { console.log( chalk.green( `Task complexity analysis complete. Report written to ${outputPath}` ) ); const highComplexity = 0; const mediumComplexity = 0; const lowComplexity = 0; const totalAnalyzed = 0; console.log('\nComplexity Analysis Summary:'); console.log('----------------------------'); console.log(`Tasks in input file: ${originalTaskCount}`); console.log(`Tasks successfully analyzed: ${totalAnalyzed}`); console.log(`High complexity tasks: ${highComplexity}`); console.log(`Medium complexity tasks: ${mediumComplexity}`); console.log(`Low complexity tasks: ${lowComplexity}`); console.log( `Sum verification: ${highComplexity + mediumComplexity + lowComplexity} (should equal ${totalAnalyzed})` ); console.log(`Research-backed analysis: ${useResearch ? 'Yes' : 'No'}`); console.log( `\nSee ${outputPath} for the full report and expansion commands.` ); console.log( boxen( chalk.white.bold('Suggested Next Steps:') + '\n\n' + `${chalk.cyan('1.')} Run ${chalk.yellow('task-master complexity-report')} to review detailed findings\n` + `${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down complex tasks\n` + `${chalk.cyan('3.')} Run ${chalk.yellow('task-master expand --all')} to expand all pending tasks based on complexity`, { padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: { top: 1 } } ) ); } return { report: emptyReport, telemetryData: null }; } // Continue with regular analysis path const prompt = generateInternalComplexityAnalysisPrompt(tasksData); const systemPrompt = 'You are an expert software architect and project manager analyzing task complexity. Respond only with the requested valid JSON array.'; let loadingIndicator = null; if (outputFormat === 'text') { loadingIndicator = startLoadingIndicator( `${useResearch ? 'Researching' : 'Analyzing'} the complexity of your tasks with AI...\n` ); } let aiServiceResponse = null; let complexityAnalysis = null; try { // Use agentic manual mode directly - completely independent from external APIs reportLog('Using agentic manual mode for complexity analysis (external APIs disabled)', 'info'); // Generate complexity analysis using agentic manual mode const analysisText = await generateComplexityAnalysisAgenticMode( tasksToAnalyze, threshold, useResearch ); aiServiceResponse = { mainResult: analysisText, telemetryData: null }; if (loadingIndicator) { stopLoadingIndicator(loadingIndicator); loadingIndicator = null; } if (outputFormat === 'text') { readline.clearLine(process.stdout, 0); readline.cursorTo(process.stdout, 0); console.log( chalk.green('AI service call complete. Parsing response...') ); } reportLog(`Parsing complexity analysis from text response...`, 'info'); try { let cleanedResponse = aiServiceResponse.mainResult; cleanedResponse = cleanedResponse.trim(); const codeBlockMatch = cleanedResponse.match( /```(?:json)?\s*([\s\S]*?)\s*```/ ); if (codeBlockMatch) { cleanedResponse = codeBlockMatch[1].trim(); } else { const firstBracket = cleanedResponse.indexOf('['); const lastBracket = cleanedResponse.lastIndexOf(']'); if (firstBracket !== -1 && lastBracket > firstBracket) { cleanedResponse = cleanedResponse.substring( firstBracket, lastBracket + 1 ); } else { reportLog( 'Warning: Response does not appear to be a JSON array.', 'warn' ); } } if (outputFormat === 'text' && getDebugFlag(session)) { console.log(chalk.gray('Attempting to parse cleaned JSON...')); console.log(chalk.gray('Cleaned response (first 100 chars):')); console.log(chalk.gray(cleanedResponse.substring(0, 100))); console.log(chalk.gray('Last 100 chars:')); console.log( chalk.gray(cleanedResponse.substring(cleanedResponse.length - 100)) ); } complexityAnalysis = JSON.parse(cleanedResponse); } catch (parseError) { if (loadingIndicator) stopLoadingIndicator(loadingIndicator); reportLog( `Error parsing complexity analysis JSON: ${parseError.message}`, 'error' ); if (outputFormat === 'text') { console.error( chalk.red( `Error parsing complexity analysis JSON: ${parseError.message}` ) ); } throw parseError; } const taskIds = tasksData.tasks.map((t) => t.id); const analysisTaskIds = complexityAnalysis.map((a) => a.taskId); const missingTaskIds = taskIds.filter( (id) => !analysisTaskIds.includes(id) ); if (missingTaskIds.length > 0) { reportLog( `Missing analysis for ${missingTaskIds.length} tasks: ${missingTaskIds.join(', ')}`, 'warn' ); if (outputFormat === 'text') { console.log( chalk.yellow( `Missing analysis for ${missingTaskIds.length} tasks: ${missingTaskIds.join(', ')}` ) ); } for (const missingId of missingTaskIds) { const missingTask = tasksData.tasks.find((t) => t.id === missingId); if (missingTask) { reportLog(`Adding default analysis for task ${missingId}`, 'info'); complexityAnalysis.push({ taskId: missingId, taskTitle: missingTask.title, complexityScore: 5, recommendedSubtasks: 3, expansionPrompt: `Break down this task with a focus on ${missingTask.title.toLowerCase()}.`, reasoning: 'Automatically added due to missing analysis in AI response.' }); } } } // Merge with existing report let finalComplexityAnalysis = []; if (existingReport && Array.isArray(existingReport.complexityAnalysis)) { // Create a map of task IDs that we just analyzed const analyzedTaskIds = new Set( complexityAnalysis.map((item) => item.taskId) ); // Keep existing entries that weren't in this analysis run const existingEntriesNotAnalyzed = existingReport.complexityAnalysis.filter( (item) => !analyzedTaskIds.has(item.taskId) ); // Combine with new analysis finalComplexityAnalysis = [ ...existingEntriesNotAnalyzed, ...complexityAnalysis ]; reportLog( `Merged ${complexityAnalysis.length} new analyses with ${existingEntriesNotAnalyzed.length} existing entries`, 'info' ); } else { // No existing report or invalid format, just use the new analysis finalComplexityAnalysis = complexityAnalysis; } const report = { meta: { generatedAt: new Date().toISOString(), tasksAnalyzed: tasksData.tasks.length, totalTasks: originalTaskCount, analysisCount: finalComplexityAnalysis.length, thresholdScore: thresholdScore, projectName: getProjectName(session), usedResearch: useResearch }, complexityAnalysis: finalComplexityAnalysis }; reportLog(`Writing complexity report to ${outputPath}...`, 'info'); writeJSON(outputPath, report); reportLog( `Task complexity analysis complete. Report written to ${outputPath}`, 'success' ); if (outputFormat === 'text') { console.log( chalk.green( `Task complexity analysis complete. Report written to ${outputPath}` ) ); // Calculate statistics specifically for this analysis run const highComplexity = complexityAnalysis.filter( (t) => t.complexityScore >= 8 ).length; const mediumComplexity = complexityAnalysis.filter( (t) => t.complexityScore >= 5 && t.complexityScore < 8 ).length; const lowComplexity = complexityAnalysis.filter( (t) => t.complexityScore < 5 ).length; const totalAnalyzed = complexityAnalysis.length; console.log('\nCurrent Analysis Summary:'); console.log('----------------------------'); console.log(`Tasks analyzed in this run: ${totalAnalyzed}`); console.log(`High complexity tasks: ${highComplexity}`); console.log(`Medium complexity tasks: ${mediumComplexity}`); console.log(`Low complexity tasks: ${lowComplexity}`); if (existingReport) { console.log('\nUpdated Report Summary:'); console.log('----------------------------'); console.log( `Total analyses in report: ${finalComplexityAnalysis.length}` ); console.log( `Analyses from previous runs: ${finalComplexityAnalysis.length - totalAnalyzed}` ); console.log(`New/updated analyses: ${totalAnalyzed}`); } console.log(`Research-backed analysis: ${useResearch ? 'Yes' : 'No'}`); console.log( `\nSee ${outputPath} for the full report and expansion commands.` ); console.log( boxen( chalk.white.bold('Suggested Next Steps:') + '\n\n' + `${chalk.cyan('1.')} Run ${chalk.yellow('task-master complexity-report')} to review detailed findings\n` + `${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down complex tasks\n` + `${chalk.cyan('3.')} Run ${chalk.yellow('task-master expand --all')} to expand all pending tasks based on complexity`, { padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: { top: 1 } } ) ); if (getDebugFlag(session)) { console.debug( chalk.gray( `Final analysis object: ${JSON.stringify(report, null, 2)}` ) ); } if (aiServiceResponse.telemetryData) { displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli'); } } return { report: report, telemetryData: aiServiceResponse?.telemetryData }; } catch (aiError) { if (loadingIndicator) stopLoadingIndicator(loadingIndicator); reportLog(`Error during AI service call: ${aiError.message}`, 'error'); if (outputFormat === 'text') { console.error( chalk.red(`Error during AI service call: ${aiError.message}`) ); if (aiError.message.includes('API key')) { console.log( chalk.yellow( '\nPlease ensure your API keys are correctly configured in .env or ~/.taskmaster/.env' ) ); console.log( chalk.yellow("Run 'task-master models --setup' if needed.") ); } } throw aiError; } } catch (error) { reportLog(`Error analyzing task complexity: ${error.message}`, 'error'); if (outputFormat === 'text') { console.error( chalk.red(`Error analyzing task complexity: ${error.message}`) ); if (getDebugFlag(session)) { console.error(error); } process.exit(1); } else { throw error; } } } /** * Generate complexity analysis using agentic manual mode * @param {Array} tasks - Tasks to analyze * @param {number} threshold - Complexity threshold * @param {boolean} useResearch - Whether research mode was requested * @returns {Promise<string>} Generated complexity analysis */ async function generateComplexityAnalysisAgenticMode(tasks, threshold, useResearch = false) { const analysisResults = []; tasks.forEach(task => { // Calculate complexity score based on various factors let complexityScore = 1; // Base score // Factor 1: Title complexity (keywords indicating complexity) const titleLower = task.title.toLowerCase(); const complexKeywords = ['integration', 'architecture', 'system', 'framework', 'migration', 'optimization', 'security', 'performance', 'scalability']; const complexityKeywordCount = complexKeywords.filter(keyword => titleLower.includes(keyword)).length; complexityScore += complexityKeywordCount * 1.5; // Factor 2: Description length and content const descriptionLength = (task.description || '').length; if (descriptionLength > 200) complexityScore += 1; if (descriptionLength > 500) complexityScore += 1; // Factor 3: Details complexity const detailsLength = (task.details || '').length; if (detailsLength > 500) complexityScore += 1; if (detailsLength > 1000) complexityScore += 1; // Factor 4: Dependencies count const dependencyCount = (task.dependencies || []).length; complexityScore += dependencyCount * 0.5; // Factor 5: Subtasks count const subtaskCount = (task.subtasks || []).length; complexityScore += subtaskCount * 0.3; // Factor 6: Priority impact if (task.priority === 'high') complexityScore += 1; // Cap the score at 10 complexityScore = Math.min(Math.round(complexityScore * 10) / 10, 10); // Determine if expansion is recommended const recommendExpansion = complexityScore >= threshold; // Generate expansion recommendations let expansionRecommendations = []; if (recommendExpansion) { expansionRecommendations = [ "Break down into smaller, manageable subtasks", "Define clear acceptance criteria for each component", "Identify potential risks and mitigation strategies", "Create detailed implementation timeline", "Establish testing and validation checkpoints" ]; // Add specific recommendations based on task content if (titleLower.includes('api') || titleLower.includes('service')) { expansionRecommendations.push("Design API endpoints and data models"); expansionRecommendations.push("Plan authentication and authorization"); } if (titleLower.includes('database') || titleLower.includes('data')) { expansionRecommendations.push("Design database schema and migrations"); expansionRecommendations.push("Plan data validation and backup strategies"); } if (titleLower.includes('ui') || titleLower.includes('frontend')) { expansionRecommendations.push("Create wireframes and user flow diagrams"); expansionRecommendations.push("Plan responsive design and accessibility"); } } // Generate reasoning const reasoning = `Complexity factors: ${complexityKeywordCount} complex keywords, ${descriptionLength} char description, ${dependencyCount} dependencies, ${subtaskCount} subtasks, ${task.priority} priority`; analysisResults.push({ taskId: task.id, title: task.title, complexityScore, recommendExpansion, reasoning, expansionRecommendations: recommendExpansion ? expansionRecommendations.slice(0, 5) : [] }); }); // Generate the analysis report text let reportText = `# Task Complexity Analysis Report Generated: ${new Date().toISOString().split('T')[0]} Threshold: ${threshold} Total Tasks Analyzed: ${tasks.length} ${useResearch ? 'Research Mode: Enabled' : ''} ## Analysis Results `; analysisResults.forEach(result => { reportText += `### Task ${result.taskId}: ${result.title} - **Complexity Score**: ${result.complexityScore}/10 - **Expansion Recommended**: ${result.recommendExpansion ? 'Yes' : 'No'} - **Reasoning**: ${result.reasoning} `; if (result.recommendExpansion && result.expansionRecommendations.length > 0) { reportText += `**Expansion Recommendations**: ${result.expansionRecommendations.map(rec => `- ${rec}`).join('\n')} `; } reportText += '---\n\n'; }); // Add summary const highComplexityTasks = analysisResults.filter(r => r.complexityScore >= threshold); reportText += `## Summary - **High Complexity Tasks**: ${highComplexityTasks.length}/${analysisResults.length} - **Average Complexity Score**: ${(analysisResults.reduce((sum, r) => sum + r.complexityScore, 0) / analysisResults.length).toFixed(1)} - **Tasks Requiring Expansion**: ${highComplexityTasks.length} `; if (useResearch) { reportText += `## Research-Enhanced Insights - Industry best practices suggest breaking down tasks with complexity scores above ${threshold} - Modern development methodologies recommend iterative task decomposition - Quality assurance improves significantly with smaller, focused task units - Team productivity increases with well-defined, manageable task scope `; } return reportText; } export default analyzeTaskComplexity;