import chalk from 'chalk'; import boxen from 'boxen'; import readline from 'readline'; 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'; /** * 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": , "taskTitle": "", "complexityScore": , "recommendedSubtasks": , "expansionPrompt": "", "reasoning": "" }, ... ] 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 {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 || 'tasks/tasks.json'; const outputPath = options.output || 'scripts/task-complexity-report.json'; const thresholdScore = parseFloat(options.threshold || '5'); const useResearch = options.research || false; const projectRoot = options.projectRoot; 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; if (options._filteredTasksData) { tasksData = options._filteredTasksData; originalTaskCount = options._originalTaskCount || tasksData.tasks.length; if (!options._originalTaskCount) { try { const originalData = readJSON(tasksPath); if (originalData && originalData.tasks) { originalTaskCount = originalData.tasks.length; } } catch (e) { log('warn', `Could not read original tasks file: ${e.message}`); } } } else { tasksData = readJSON(tasksPath); if ( !tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks) || tasksData.tasks.length === 0 ) { throw new Error('No tasks found in the tasks file'); } originalTaskCount = tasksData.tasks.length; const activeStatuses = ['pending', 'blocked', 'in-progress']; const filteredTasks = tasksData.tasks.filter((task) => activeStatuses.includes(task.status?.toLowerCase() || 'pending') ); tasksData = { ...tasksData, tasks: filteredTasks, _originalTaskCount: originalTaskCount }; } const skippedCount = originalTaskCount - tasksData.tasks.length; reportLog( `Found ${originalTaskCount} total tasks in the task file.`, 'info' ); 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)); } } if (tasksData.tasks.length === 0) { const emptyReport = { meta: { generatedAt: new Date().toISOString(), tasksAnalyzed: 0, thresholdScore: thresholdScore, projectName: getProjectName(session), usedResearch: useResearch }, complexityAnalysis: [] }; reportLog(`Writing empty 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=')} 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 emptyReport; } 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 { const role = useResearch ? 'research' : 'main'; aiServiceResponse = await generateTextService({ prompt, systemPrompt, role, session, projectRoot, commandName: 'analyze-complexity', outputType: mcpLog ? 'mcp' : 'cli' }); 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.' }); } } } const report = { meta: { generatedAt: new Date().toISOString(), tasksAnalyzed: tasksData.tasks.length, thresholdScore: thresholdScore, projectName: getProjectName(session), usedResearch: useResearch }, complexityAnalysis: complexityAnalysis }; 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}` ) ); 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('\nComplexity Analysis Summary:'); console.log('----------------------------'); console.log( `Active tasks sent for analysis: ${tasksData.tasks.length}` ); 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=')} 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; } } } export default analyzeTaskComplexity;