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
JavaScript
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;