From 58b417a8ce697e655f749ca4d759b1c20014c523 Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Fri, 16 May 2025 23:24:25 +0200 Subject: [PATCH] Add complexity score to task (#528) * feat: added complexity score handling to list tasks * feat: added handling for complexity score in find task by id * test: remove console dir * chore: add changeset * format: fixed formatting issues * ref: reorder imports * feat: updated handling for findTaskById to take complexityReport as input * test: fix findTaskById complexity report testcases * fix: added handling for complexity report path * chore: add changeset * fix: moved complexity report handling to list tasks rather than list tasks direct * fix: add complexity handling to next task in list command * fix: added handling for show cli * fix: fixed next cli command handling * fix: fixed handling for complexity report path in mcp * feat: added handling to get-task * feat: added handling for next-task in mcp * feat: add handling for report path override * chore: remove unecessary changeset * ref: remove unecessary comments * feat: update list and find next task * fix: fixed running tests * fix: fixed findTaskById * fix: fixed findTaskById and tests * fix: fixed addComplexityToTask util * fix: fixed mcp server project root input * chore: cleanup --------- Co-authored-by: Shrey Paharia --- .changeset/social-masks-fold.md | 5 + .../src/core/direct-functions/list-tasks.js | 4 +- .../src/core/direct-functions/next-task.js | 12 +- .../src/core/direct-functions/show-task.js | 12 +- mcp-server/src/core/utils/path-utils.js | 43 +++++++ mcp-server/src/tools/get-task.js | 24 +++- mcp-server/src/tools/get-tasks.js | 25 +++- mcp-server/src/tools/next-task.js | 25 +++- scripts/modules/commands.js | 24 +++- scripts/modules/task-manager.js | 5 +- .../modules/task-manager/find-next-task.js | 20 +++- scripts/modules/task-manager/list-tasks.js | 74 +++++++++--- scripts/modules/ui.js | 66 +++++++++-- scripts/modules/utils.js | 57 +++++++-- .../mcp-server/direct-functions.test.js | 108 ++++++++++++++---- tests/unit/task-finder.test.js | 60 +++++++++- 16 files changed, 490 insertions(+), 74 deletions(-) create mode 100644 .changeset/social-masks-fold.md diff --git a/.changeset/social-masks-fold.md b/.changeset/social-masks-fold.md new file mode 100644 index 00000000..4a2d4158 --- /dev/null +++ b/.changeset/social-masks-fold.md @@ -0,0 +1,5 @@ +--- +'task-master-ai': minor +--- + +Display task complexity scores in task lists, next task, and task details views. diff --git a/mcp-server/src/core/direct-functions/list-tasks.js b/mcp-server/src/core/direct-functions/list-tasks.js index 5ef7487f..49aa42bf 100644 --- a/mcp-server/src/core/direct-functions/list-tasks.js +++ b/mcp-server/src/core/direct-functions/list-tasks.js @@ -18,7 +18,7 @@ import { */ export async function listTasksDirect(args, log) { // Destructure the explicit tasksJsonPath from args - const { tasksJsonPath, status, withSubtasks } = args; + const { tasksJsonPath, reportPath, status, withSubtasks } = args; if (!tasksJsonPath) { log.error('listTasksDirect called without tasksJsonPath'); @@ -49,6 +49,7 @@ export async function listTasksDirect(args, log) { const resultData = listTasks( tasksJsonPath, statusFilter, + reportPath, withSubtasksFilter, 'json' ); @@ -63,6 +64,7 @@ export async function listTasksDirect(args, log) { } }; } + log.info( `Core listTasks function retrieved ${resultData.tasks.length} tasks` ); diff --git a/mcp-server/src/core/direct-functions/next-task.js b/mcp-server/src/core/direct-functions/next-task.js index b1993942..3bc80d48 100644 --- a/mcp-server/src/core/direct-functions/next-task.js +++ b/mcp-server/src/core/direct-functions/next-task.js @@ -4,7 +4,10 @@ */ import { findNextTask } from '../../../../scripts/modules/task-manager.js'; -import { readJSON } from '../../../../scripts/modules/utils.js'; +import { + readJSON, + readComplexityReport +} from '../../../../scripts/modules/utils.js'; import { enableSilentMode, disableSilentMode @@ -20,7 +23,7 @@ import { */ export async function nextTaskDirect(args, log) { // Destructure expected args - const { tasksJsonPath } = args; + const { tasksJsonPath, reportPath } = args; if (!tasksJsonPath) { log.error('nextTaskDirect called without tasksJsonPath'); @@ -55,8 +58,11 @@ export async function nextTaskDirect(args, log) { }; } + // Read the complexity report + const complexityReport = readComplexityReport(reportPath); + // Find the next task - const nextTask = findNextTask(data.tasks); + const nextTask = findNextTask(data.tasks, complexityReport); if (!nextTask) { log.info( diff --git a/mcp-server/src/core/direct-functions/show-task.js b/mcp-server/src/core/direct-functions/show-task.js index 1bee0636..27c0e2a9 100644 --- a/mcp-server/src/core/direct-functions/show-task.js +++ b/mcp-server/src/core/direct-functions/show-task.js @@ -3,7 +3,11 @@ * Direct function implementation for showing task details */ -import { findTaskById, readJSON } from '../../../../scripts/modules/utils.js'; +import { + findTaskById, + readComplexityReport, + readJSON +} from '../../../../scripts/modules/utils.js'; import { findTasksJsonPath } from '../utils/path-utils.js'; /** @@ -12,6 +16,7 @@ import { findTasksJsonPath } from '../utils/path-utils.js'; * @param {Object} args - Command arguments. * @param {string} args.id - Task ID to show. * @param {string} [args.file] - Optional path to the tasks file (passed to findTasksJsonPath). + * @param {string} args.reportPath - Explicit path to the complexity report file. * @param {string} [args.status] - Optional status to filter subtasks by. * @param {string} args.projectRoot - Absolute path to the project root directory (already normalized by tool). * @param {Object} log - Logger object. @@ -22,7 +27,7 @@ export async function showTaskDirect(args, log) { // Destructure session from context if needed later, otherwise ignore // const { session } = context; // Destructure projectRoot and other args. projectRoot is assumed normalized. - const { id, file, status, projectRoot } = args; + const { id, file, reportPath, status, projectRoot } = args; log.info( `Showing task direct function. ID: ${id}, File: ${file}, Status Filter: ${status}, ProjectRoot: ${projectRoot}` @@ -59,9 +64,12 @@ export async function showTaskDirect(args, log) { }; } + const complexityReport = readComplexityReport(reportPath); + const { task, originalSubtaskCount } = findTaskById( tasksData.tasks, id, + complexityReport, status ); diff --git a/mcp-server/src/core/utils/path-utils.js b/mcp-server/src/core/utils/path-utils.js index 3d362a6d..bc4ef938 100644 --- a/mcp-server/src/core/utils/path-utils.js +++ b/mcp-server/src/core/utils/path-utils.js @@ -339,6 +339,49 @@ export function findPRDDocumentPath(projectRoot, explicitPath, log) { return null; } +export function findComplexityReportPath(projectRoot, explicitPath, log) { + // If explicit path is provided, check if it exists + if (explicitPath) { + const fullPath = path.isAbsolute(explicitPath) + ? explicitPath + : path.resolve(projectRoot, explicitPath); + + if (fs.existsSync(fullPath)) { + log.info(`Using provided PRD document path: ${fullPath}`); + return fullPath; + } else { + log.warn( + `Provided PRD document path not found: ${fullPath}, will search for alternatives` + ); + } + } + + // Common locations and file patterns for PRD documents + const commonLocations = [ + '', // Project root + 'scripts/' + ]; + + const commonFileNames = [ + 'complexity-report.json', + 'task-complexity-report.json' + ]; + + // Check all possible combinations + for (const location of commonLocations) { + for (const fileName of commonFileNames) { + const potentialPath = path.join(projectRoot, location, fileName); + if (fs.existsSync(potentialPath)) { + log.info(`Found PRD document at: ${potentialPath}`); + return potentialPath; + } + } + } + + log.warn(`No PRD document found in common locations within ${projectRoot}`); + return null; +} + /** * Resolves the tasks output directory path * @param {string} projectRoot - The project root directory diff --git a/mcp-server/src/tools/get-task.js b/mcp-server/src/tools/get-task.js index bf46d7e8..1bdc120d 100644 --- a/mcp-server/src/tools/get-task.js +++ b/mcp-server/src/tools/get-task.js @@ -10,7 +10,10 @@ import { withNormalizedProjectRoot } from './utils.js'; import { showTaskDirect } from '../core/task-master-core.js'; -import { findTasksJsonPath } from '../core/utils/path-utils.js'; +import { + findTasksJsonPath, + findComplexityReportPath +} from '../core/utils/path-utils.js'; /** * Custom processor function that removes allTasks from the response @@ -50,6 +53,12 @@ export function registerShowTaskTool(server) { .string() .optional() .describe('Path to the tasks file relative to project root'), + complexityReport: z + .string() + .optional() + .describe( + 'Path to the complexity report file (relative to project root or absolute)' + ), projectRoot: z .string() .optional() @@ -81,9 +90,22 @@ export function registerShowTaskTool(server) { } // Call the direct function, passing the normalized projectRoot + // Resolve the path to complexity report + let complexityReportPath; + try { + complexityReportPath = findComplexityReportPath( + projectRoot, + args.complexityReport, + log + ); + } catch (error) { + log.error(`Error finding complexity report: ${error.message}`); + } const result = await showTaskDirect( { tasksJsonPath: tasksJsonPath, + reportPath: complexityReportPath, + // Pass other relevant args id: id, status: status, projectRoot: projectRoot diff --git a/mcp-server/src/tools/get-tasks.js b/mcp-server/src/tools/get-tasks.js index 24d592ba..c43a554b 100644 --- a/mcp-server/src/tools/get-tasks.js +++ b/mcp-server/src/tools/get-tasks.js @@ -10,7 +10,10 @@ import { withNormalizedProjectRoot } from './utils.js'; import { listTasksDirect } from '../core/task-master-core.js'; -import { findTasksJsonPath } from '../core/utils/path-utils.js'; +import { + findTasksJsonPath, + findComplexityReportPath +} from '../core/utils/path-utils.js'; /** * Register the getTasks tool with the MCP server @@ -38,6 +41,12 @@ export function registerListTasksTool(server) { .describe( 'Path to the tasks file (relative to project root or absolute)' ), + complexityReport: z + .string() + .optional() + .describe( + 'Path to the complexity report file (relative to project root or absolute)' + ), projectRoot: z .string() .describe('The directory of the project. Must be an absolute path.') @@ -60,11 +69,23 @@ export function registerListTasksTool(server) { ); } + // Resolve the path to complexity report + let complexityReportPath; + try { + complexityReportPath = findComplexityReportPath( + args.projectRoot, + args.complexityReport, + log + ); + } catch (error) { + log.error(`Error finding complexity report: ${error.message}`); + } const result = await listTasksDirect( { tasksJsonPath: tasksJsonPath, status: args.status, - withSubtasks: args.withSubtasks + withSubtasks: args.withSubtasks, + reportPath: complexityReportPath }, log ); diff --git a/mcp-server/src/tools/next-task.js b/mcp-server/src/tools/next-task.js index b69692a9..3118180c 100644 --- a/mcp-server/src/tools/next-task.js +++ b/mcp-server/src/tools/next-task.js @@ -10,7 +10,10 @@ import { withNormalizedProjectRoot } from './utils.js'; import { nextTaskDirect } from '../core/task-master-core.js'; -import { findTasksJsonPath } from '../core/utils/path-utils.js'; +import { + findTasksJsonPath, + findComplexityReportPath +} from '../core/utils/path-utils.js'; /** * Register the next-task tool with the MCP server @@ -23,6 +26,12 @@ export function registerNextTaskTool(server) { 'Find the next task to work on based on dependencies and status', parameters: z.object({ file: z.string().optional().describe('Absolute path to the tasks file'), + complexityReport: z + .string() + .optional() + .describe( + 'Path to the complexity report file (relative to project root or absolute)' + ), projectRoot: z .string() .describe('The directory of the project. Must be an absolute path.') @@ -45,9 +54,21 @@ export function registerNextTaskTool(server) { ); } + // Resolve the path to complexity report + let complexityReportPath; + try { + complexityReportPath = findComplexityReportPath( + args.projectRoot, + args.complexityReport, + log + ); + } catch (error) { + log.error(`Error finding complexity report: ${error.message}`); + } const result = await nextTaskDirect( { - tasksJsonPath: tasksJsonPath + tasksJsonPath: tasksJsonPath, + reportPath: complexityReportPath }, log ); diff --git a/scripts/modules/commands.js b/scripts/modules/commands.js index 2ccc2412..e8de4462 100644 --- a/scripts/modules/commands.js +++ b/scripts/modules/commands.js @@ -1072,10 +1072,16 @@ function registerCommands(programInstance) { .command('list') .description('List all tasks') .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') + .option( + '-r, --report ', + 'Path to the complexity report file', + 'scripts/task-complexity-report.json' + ) .option('-s, --status ', 'Filter by status') .option('--with-subtasks', 'Show subtasks for each task') .action(async (options) => { const tasksPath = options.file; + const reportPath = options.report; const statusFilter = options.status; const withSubtasks = options.withSubtasks || false; @@ -1087,7 +1093,7 @@ function registerCommands(programInstance) { console.log(chalk.blue('Including subtasks in listing')); } - await listTasks(tasksPath, statusFilter, withSubtasks); + await listTasks(tasksPath, statusFilter, reportPath, withSubtasks); }); // expand command @@ -1393,9 +1399,15 @@ function registerCommands(programInstance) { `Show the next task to work on based on dependencies and status${chalk.reset('')}` ) .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') + .option( + '-r, --report ', + 'Path to the complexity report file', + 'scripts/task-complexity-report.json' + ) .action(async (options) => { const tasksPath = options.file; - await displayNextTask(tasksPath); + const reportPath = options.report; + await displayNextTask(tasksPath, reportPath); }); // show command @@ -1408,6 +1420,11 @@ function registerCommands(programInstance) { .option('-i, --id ', 'Task ID to show') .option('-s, --status ', 'Filter subtasks by status') // ADDED status option .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') + .option( + '-r, --report ', + 'Path to the complexity report file', + 'scripts/task-complexity-report.json' + ) .action(async (taskId, options) => { const idArg = taskId || options.id; const statusFilter = options.status; // ADDED: Capture status filter @@ -1418,8 +1435,9 @@ function registerCommands(programInstance) { } const tasksPath = options.file; + const reportPath = options.report; // PASS statusFilter to the display function - await displayTaskById(tasksPath, idArg, statusFilter); + await displayTaskById(tasksPath, idArg, reportPath, statusFilter); }); // add-dependency command diff --git a/scripts/modules/task-manager.js b/scripts/modules/task-manager.js index 44636894..9da48972 100644 --- a/scripts/modules/task-manager.js +++ b/scripts/modules/task-manager.js @@ -23,7 +23,7 @@ import updateSubtaskById from './task-manager/update-subtask-by-id.js'; import removeTask from './task-manager/remove-task.js'; import taskExists from './task-manager/task-exists.js'; import isTaskDependentOn from './task-manager/is-task-dependent.js'; - +import { readComplexityReport } from './utils.js'; // Export task manager functions export { parsePRD, @@ -45,5 +45,6 @@ export { removeTask, findTaskById, taskExists, - isTaskDependentOn + isTaskDependentOn, + readComplexityReport }; diff --git a/scripts/modules/task-manager/find-next-task.js b/scripts/modules/task-manager/find-next-task.js index c9bcb422..1f19292c 100644 --- a/scripts/modules/task-manager/find-next-task.js +++ b/scripts/modules/task-manager/find-next-task.js @@ -1,3 +1,6 @@ +import { log } from '../utils.js'; +import { addComplexityToTask } from '../utils.js'; + /** * Return the next work item: * • Prefer an eligible SUBTASK that belongs to any parent task @@ -15,9 +18,10 @@ * ─ parentId → number (present only when it's a subtask) * * @param {Object[]} tasks – full array of top-level tasks, each may contain .subtasks[] + * @param {Object} [complexityReport=null] - Optional complexity report object * @returns {Object|null} – next work item or null if nothing is eligible */ -function findNextTask(tasks) { +function findNextTask(tasks, complexityReport = null) { // ---------- helpers ---------------------------------------------------- const priorityValues = { high: 3, medium: 2, low: 1 }; @@ -91,7 +95,14 @@ function findNextTask(tasks) { if (aPar !== bPar) return aPar - bPar; return aSub - bSub; }); - return candidateSubtasks[0]; + const nextTask = candidateSubtasks[0]; + + // Add complexity to the task before returning + if (nextTask && complexityReport) { + addComplexityToTask(nextTask, complexityReport); + } + + return nextTask; } // ---------- 2) fall back to top-level tasks (original logic) ------------ @@ -116,6 +127,11 @@ function findNextTask(tasks) { return a.id - b.id; })[0]; + // Add complexity to the task before returning + if (nextTask && complexityReport) { + addComplexityToTask(nextTask, complexityReport); + } + return nextTask; } diff --git a/scripts/modules/task-manager/list-tasks.js b/scripts/modules/task-manager/list-tasks.js index fb1367c1..1aea9fee 100644 --- a/scripts/modules/task-manager/list-tasks.js +++ b/scripts/modules/task-manager/list-tasks.js @@ -2,13 +2,20 @@ import chalk from 'chalk'; import boxen from 'boxen'; import Table from 'cli-table3'; -import { log, readJSON, truncate } from '../utils.js'; +import { + log, + readJSON, + truncate, + readComplexityReport, + addComplexityToTask +} from '../utils.js'; import findNextTask from './find-next-task.js'; import { displayBanner, getStatusWithColor, formatDependenciesWithStatus, + getComplexityWithColor, createProgressBar } from '../ui.js'; @@ -16,6 +23,7 @@ import { * List all tasks * @param {string} tasksPath - Path to the tasks.json file * @param {string} statusFilter - Filter by status + * @param {string} reportPath - Path to the complexity report * @param {boolean} withSubtasks - Whether to show subtasks * @param {string} outputFormat - Output format (text or json) * @returns {Object} - Task list result for json format @@ -23,6 +31,7 @@ import { function listTasks( tasksPath, statusFilter, + reportPath = null, withSubtasks = false, outputFormat = 'text' ) { @@ -37,6 +46,13 @@ function listTasks( throw new Error(`No valid tasks found in ${tasksPath}`); } + // Add complexity scores to tasks if report exists + const complexityReport = readComplexityReport(reportPath); + // Apply complexity scores to tasks + if (complexityReport && complexityReport.complexityAnalysis) { + data.tasks.forEach((task) => addComplexityToTask(task, complexityReport)); + } + // Filter tasks by status if specified const filteredTasks = statusFilter && statusFilter.toLowerCase() !== 'all' // <-- Added check for 'all' @@ -257,8 +273,8 @@ function listTasks( ); const avgDependenciesPerTask = totalDependencies / data.tasks.length; - // Find next task to work on - const nextItem = findNextTask(data.tasks); + // Find next task to work on, passing the complexity report + const nextItem = findNextTask(data.tasks, complexityReport); // Get terminal width - more reliable method let terminalWidth; @@ -301,8 +317,11 @@ function listTasks( `${chalk.blue('•')} ${chalk.white('Avg dependencies per task:')} ${avgDependenciesPerTask.toFixed(1)}\n\n` + chalk.cyan.bold('Next Task to Work On:') + '\n' + - `ID: ${chalk.cyan(nextItem ? nextItem.id : 'N/A')} - ${nextItem ? chalk.white.bold(truncate(nextItem.title, 40)) : chalk.yellow('No task available')}\n` + - `Priority: ${nextItem ? chalk.white(nextItem.priority || 'medium') : ''} Dependencies: ${nextItem ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true) : ''}`; + `ID: ${chalk.cyan(nextItem ? nextItem.id : 'N/A')} - ${nextItem ? chalk.white.bold(truncate(nextItem.title, 40)) : chalk.yellow('No task available')} +` + + `Priority: ${nextItem ? chalk.white(nextItem.priority || 'medium') : ''} Dependencies: ${nextItem ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : ''} +` + + `Complexity: ${nextItem && nextItem.complexityScore ? getComplexityWithColor(nextItem.complexityScore) : chalk.gray('N/A')}`; // Calculate width for side-by-side display // Box borders, padding take approximately 4 chars on each side @@ -412,9 +431,16 @@ function listTasks( // Make dependencies column smaller as requested (-20%) const depsWidthPct = 20; + const complexityWidthPct = 10; + // Calculate title/description width as remaining space (+20% from dependencies reduction) const titleWidthPct = - 100 - idWidthPct - statusWidthPct - priorityWidthPct - depsWidthPct; + 100 - + idWidthPct - + statusWidthPct - + priorityWidthPct - + depsWidthPct - + complexityWidthPct; // Allow 10 characters for borders and padding const availableWidth = terminalWidth - 10; @@ -424,6 +450,9 @@ function listTasks( const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100)); const priorityWidth = Math.floor(availableWidth * (priorityWidthPct / 100)); const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100)); + const complexityWidth = Math.floor( + availableWidth * (complexityWidthPct / 100) + ); const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100)); // Create a table with correct borders and spacing @@ -433,9 +462,17 @@ function listTasks( chalk.cyan.bold('Title'), chalk.cyan.bold('Status'), chalk.cyan.bold('Priority'), - chalk.cyan.bold('Dependencies') + chalk.cyan.bold('Dependencies'), + chalk.cyan.bold('Complexity') + ], + colWidths: [ + idWidth, + titleWidth, + statusWidth, + priorityWidth, + depsWidth, + complexityWidth // Added complexity column width ], - colWidths: [idWidth, titleWidth, statusWidth, priorityWidth, depsWidth], style: { head: [], // No special styling for header border: [], // No special styling for border @@ -454,7 +491,8 @@ function listTasks( depText = formatDependenciesWithStatus( task.dependencies, data.tasks, - true + true, + complexityReport ); } else { depText = chalk.gray('None'); @@ -480,7 +518,10 @@ function listTasks( truncate(cleanTitle, titleWidth - 3), status, priorityColor(truncate(task.priority || 'medium', priorityWidth - 2)), - depText // No truncation for dependencies + depText, + task.complexityScore + ? getComplexityWithColor(task.complexityScore) + : chalk.gray('N/A') ]); // Add subtasks if requested @@ -516,6 +557,8 @@ function listTasks( // Default to regular task dependency const depTask = data.tasks.find((t) => t.id === depId); if (depTask) { + // Add complexity to depTask before checking status + addComplexityToTask(depTask, complexityReport); const isDone = depTask.status === 'done' || depTask.status === 'completed'; const isInProgress = depTask.status === 'in-progress'; @@ -541,7 +584,10 @@ function listTasks( chalk.dim(`└─ ${truncate(subtask.title, titleWidth - 5)}`), getStatusWithColor(subtask.status, true), chalk.dim('-'), - subtaskDepText // No truncation for dependencies + subtaskDepText, + subtask.complexityScore + ? chalk.gray(`${subtask.complexityScore}`) + : chalk.gray('N/A') ]); }); } @@ -597,6 +643,8 @@ function listTasks( subtasksSection = `\n\n${chalk.white.bold('Subtasks:')}\n`; subtasksSection += parentTaskForSubtasks.subtasks .map((subtask) => { + // Add complexity to subtask before display + addComplexityToTask(subtask, complexityReport); // Using a more simplified format for subtask status display const status = subtask.status || 'pending'; const statusColors = { @@ -625,8 +673,8 @@ function listTasks( '\n\n' + // Use nextItem.priority, nextItem.status, nextItem.dependencies `${chalk.white('Priority:')} ${priorityColors[nextItem.priority || 'medium'](nextItem.priority || 'medium')} ${chalk.white('Status:')} ${getStatusWithColor(nextItem.status, true)}\n` + - `${chalk.white('Dependencies:')} ${nextItem.dependencies && nextItem.dependencies.length > 0 ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true) : chalk.gray('None')}\n\n` + - // Use nextItem.description (Note: findNextTask doesn't return description, need to fetch original task/subtask for this) + `${chalk.white('Dependencies:')} ${nextItem.dependencies && nextItem.dependencies.length > 0 ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : chalk.gray('None')}\n\n` + + // Use nextTask.description (Note: findNextTask doesn't return description, need to fetch original task/subtask for this) // *** Fetching original item for description and details *** `${chalk.white('Description:')} ${getWorkItemDescription(nextItem, data.tasks)}` + subtasksSection + // <-- Subtasks are handled above now diff --git a/scripts/modules/ui.js b/scripts/modules/ui.js index a88edc98..19fc6656 100644 --- a/scripts/modules/ui.js +++ b/scripts/modules/ui.js @@ -17,7 +17,11 @@ import { isSilentMode } from './utils.js'; import fs from 'fs'; -import { findNextTask, analyzeTaskComplexity } from './task-manager.js'; +import { + findNextTask, + analyzeTaskComplexity, + readComplexityReport +} from './task-manager.js'; import { getProjectName, getDefaultSubtasks } from './config-manager.js'; import { TASK_STATUS_OPTIONS } from '../../src/constants/task-status.js'; import { getTaskMasterVersion } from '../../src/utils/getVersion.js'; @@ -264,12 +268,14 @@ function getStatusWithColor(status, forTable = false) { * @param {Array} dependencies - Array of dependency IDs * @param {Array} allTasks - Array of all tasks * @param {boolean} forConsole - Whether the output is for console display + * @param {Object|null} complexityReport - Optional pre-loaded complexity report * @returns {string} Formatted dependencies string */ function formatDependenciesWithStatus( dependencies, allTasks, - forConsole = false + forConsole = false, + complexityReport = null // Add complexityReport parameter ) { if ( !dependencies || @@ -333,7 +339,11 @@ function formatDependenciesWithStatus( typeof depId === 'string' ? parseInt(depId, 10) : depId; // Look up the task using the numeric ID - const depTaskResult = findTaskById(allTasks, numericDepId); + const depTaskResult = findTaskById( + allTasks, + numericDepId, + complexityReport + ); const depTask = depTaskResult.task; // Access the task object from the result if (!depTask) { @@ -752,7 +762,7 @@ function truncateString(str, maxLength) { * Display the next task to work on * @param {string} tasksPath - Path to the tasks.json file */ -async function displayNextTask(tasksPath) { +async function displayNextTask(tasksPath, complexityReportPath = null) { displayBanner(); // Read the tasks file @@ -762,8 +772,11 @@ async function displayNextTask(tasksPath) { process.exit(1); } + // Read complexity report once + const complexityReport = readComplexityReport(complexityReportPath); + // Find the next task - const nextTask = findNextTask(data.tasks); + const nextTask = findNextTask(data.tasks, complexityReport); if (!nextTask) { console.log( @@ -824,7 +837,18 @@ async function displayNextTask(tasksPath) { ], [ chalk.cyan.bold('Dependencies:'), - formatDependenciesWithStatus(nextTask.dependencies, data.tasks, true) + formatDependenciesWithStatus( + nextTask.dependencies, + data.tasks, + true, + complexityReport + ) + ], + [ + chalk.cyan.bold('Complexity:'), + nextTask.complexityScore + ? getComplexityWithColor(nextTask.complexityScore) + : chalk.gray('N/A') ], [chalk.cyan.bold('Description:'), nextTask.description] ); @@ -992,7 +1016,12 @@ async function displayNextTask(tasksPath) { * @param {string|number} taskId - The ID of the task to display * @param {string} [statusFilter] - Optional status to filter subtasks by */ -async function displayTaskById(tasksPath, taskId, statusFilter = null) { +async function displayTaskById( + tasksPath, + taskId, + complexityReportPath = null, + statusFilter = null +) { displayBanner(); // Read the tasks file @@ -1002,11 +1031,15 @@ async function displayTaskById(tasksPath, taskId, statusFilter = null) { process.exit(1); } + // Read complexity report once + const complexityReport = readComplexityReport(complexityReportPath); + // Find the task by ID, applying the status filter if provided // Returns { task, originalSubtaskCount, originalSubtasks } const { task, originalSubtaskCount, originalSubtasks } = findTaskById( data.tasks, taskId, + complexityReport, statusFilter ); @@ -1061,6 +1094,12 @@ async function displayTaskById(tasksPath, taskId, statusFilter = null) { chalk.cyan.bold('Status:'), getStatusWithColor(task.status || 'pending', true) ], + [ + chalk.cyan.bold('Complexity:'), + task.complexityScore + ? getComplexityWithColor(task.complexityScore) + : chalk.gray('N/A') + ], [ chalk.cyan.bold('Description:'), task.description || 'No description provided.' @@ -1139,7 +1178,18 @@ async function displayTaskById(tasksPath, taskId, statusFilter = null) { [chalk.cyan.bold('Priority:'), priorityColor(task.priority || 'medium')], [ chalk.cyan.bold('Dependencies:'), - formatDependenciesWithStatus(task.dependencies, data.tasks, true) + formatDependenciesWithStatus( + task.dependencies, + data.tasks, + true, + complexityReport + ) + ], + [ + chalk.cyan.bold('Complexity:'), + task.complexityScore + ? getComplexityWithColor(task.complexityScore) + : chalk.gray('N/A') ], [chalk.cyan.bold('Description:'), task.description] ); diff --git a/scripts/modules/utils.js b/scripts/modules/utils.js index 64432f6f..d2b7354c 100644 --- a/scripts/modules/utils.js +++ b/scripts/modules/utils.js @@ -275,6 +275,22 @@ function findTaskInComplexityReport(report, taskId) { return report.complexityAnalysis.find((task) => task.taskId === taskId); } +function addComplexityToTask(task, complexityReport) { + let taskId; + if (task.isSubtask) { + taskId = task.parentTask.id; + } else if (task.parentId) { + taskId = task.parentId; + } else { + taskId = task.id; + } + + const taskAnalysis = findTaskInComplexityReport(complexityReport, taskId); + if (taskAnalysis) { + task.complexityScore = taskAnalysis.complexityScore; + } +} + /** * Checks if a task exists in the tasks array * @param {Array} tasks - The tasks array @@ -325,10 +341,17 @@ function formatTaskId(id) { * Finds a task by ID in the tasks array. Optionally filters subtasks by status. * @param {Array} tasks - The tasks array * @param {string|number} taskId - The task ID to find + * @param {Object|null} complexityReport - Optional pre-loaded complexity report + * @returns {Object|null} The task object or null if not found * @param {string} [statusFilter] - Optional status to filter subtasks by * @returns {{task: Object|null, originalSubtaskCount: number|null}} The task object (potentially with filtered subtasks) and the original subtask count if filtered, or nulls if not found. */ -function findTaskById(tasks, taskId, statusFilter = null) { +function findTaskById( + tasks, + taskId, + complexityReport = null, + statusFilter = null +) { if (!taskId || !tasks || !Array.isArray(tasks)) { return { task: null, originalSubtaskCount: null }; } @@ -356,10 +379,17 @@ function findTaskById(tasks, taskId, statusFilter = null) { subtask.isSubtask = true; } - // Return the found subtask (or null) and null for originalSubtaskCount + // If we found a task, check for complexity data + if (subtask && complexityReport) { + addComplexityToTask(subtask, complexityReport); + } + return { task: subtask || null, originalSubtaskCount: null }; } + let taskResult = null; + let originalSubtaskCount = null; + // Find the main task const id = parseInt(taskId, 10); const task = tasks.find((t) => t.id === id) || null; @@ -369,6 +399,8 @@ function findTaskById(tasks, taskId, statusFilter = null) { return { task: null, originalSubtaskCount: null }; } + taskResult = task; + // If task found and statusFilter provided, filter its subtasks if (statusFilter && task.subtasks && Array.isArray(task.subtasks)) { const originalSubtaskCount = task.subtasks.length; @@ -379,12 +411,18 @@ function findTaskById(tasks, taskId, statusFilter = null) { subtask.status && subtask.status.toLowerCase() === statusFilter.toLowerCase() ); - // Return the filtered task and the original count - return { task: filteredTask, originalSubtaskCount: originalSubtaskCount }; + + taskResult = filteredTask; + originalSubtaskCount = originalSubtaskCount; } - // Return original task and null count if no filter or no subtasks - return { task: task, originalSubtaskCount: null }; + // If task found and complexityReport provided, add complexity data + if (taskResult && complexityReport) { + addComplexityToTask(taskResult, complexityReport); + } + + // Return the found task and original subtask count + return { task: taskResult, originalSubtaskCount }; } /** @@ -524,10 +562,11 @@ export { findCycles, toKebabCase, detectCamelCaseFlags, - enableSilentMode, disableSilentMode, - isSilentMode, - resolveEnvVariable, + enableSilentMode, getTaskManager, + isSilentMode, + addComplexityToTask, + resolveEnvVariable, findProjectRoot }; diff --git a/tests/integration/mcp-server/direct-functions.test.js b/tests/integration/mcp-server/direct-functions.test.js index ff265ee1..d7c18822 100644 --- a/tests/integration/mcp-server/direct-functions.test.js +++ b/tests/integration/mcp-server/direct-functions.test.js @@ -3,9 +3,8 @@ */ import { jest } from '@jest/globals'; -import path from 'path'; +import path, { dirname } from 'path'; import { fileURLToPath } from 'url'; -import { dirname } from 'path'; // Get the current module's directory const __filename = fileURLToPath(import.meta.url); @@ -27,6 +26,7 @@ const mockReadJSON = jest.fn(); const mockWriteJSON = jest.fn(); const mockEnableSilentMode = jest.fn(); const mockDisableSilentMode = jest.fn(); +const mockReadComplexityReport = jest.fn().mockReturnValue(null); const mockGetAnthropicClient = jest.fn().mockReturnValue({}); const mockGetConfiguredAnthropicClient = jest.fn().mockReturnValue({}); @@ -130,6 +130,7 @@ jest.mock('../../../scripts/modules/utils.js', () => ({ writeJSON: mockWriteJSON, enableSilentMode: mockEnableSilentMode, disableSilentMode: mockDisableSilentMode, + readComplexityReport: mockReadComplexityReport, CONFIG: { model: 'claude-3-7-sonnet-20250219', maxTokens: 64000, @@ -160,15 +161,6 @@ jest.mock('../../../scripts/modules/task-manager.js', () => ({ })); // Import dependencies after mocks are set up -import fs from 'fs'; -import { - readJSON, - writeJSON, - enableSilentMode, - disableSilentMode -} from '../../../scripts/modules/utils.js'; -import { expandTask } from '../../../scripts/modules/task-manager.js'; -import { findTasksJsonPath } from '../../../mcp-server/src/core/utils/path-utils.js'; import { sampleTasks } from '../../fixtures/sample-tasks.js'; // Mock logger @@ -220,6 +212,37 @@ describe('MCP Server Direct Functions', () => { }); describe('listTasksDirect', () => { + // Sample complexity report for testing + const mockComplexityReport = { + meta: { + generatedAt: '2025-03-24T20:01:35.986Z', + tasksAnalyzed: 3, + thresholdScore: 5, + projectName: 'Test Project', + usedResearch: false + }, + complexityAnalysis: [ + { + taskId: 1, + taskTitle: 'Initialize Project', + complexityScore: 3, + recommendedSubtasks: 2 + }, + { + taskId: 2, + taskTitle: 'Create Core Functionality', + complexityScore: 8, + recommendedSubtasks: 5 + }, + { + taskId: 3, + taskTitle: 'Implement UI Components', + complexityScore: 6, + recommendedSubtasks: 4 + } + ] + }; + // Test wrapper function that doesn't rely on the actual implementation async function testListTasks(args, mockLogger) { // File not found case @@ -235,21 +258,35 @@ describe('MCP Server Direct Functions', () => { }; } + // Check for complexity report + const complexityReport = mockReadComplexityReport(); + let tasksData = [...sampleTasks.tasks]; + + // Add complexity scores if report exists + if (complexityReport && complexityReport.complexityAnalysis) { + tasksData = tasksData.map((task) => { + const analysis = complexityReport.complexityAnalysis.find( + (a) => a.taskId === task.id + ); + if (analysis) { + return { ...task, complexityScore: analysis.complexityScore }; + } + return task; + }); + } + // Success case if (!args.status && !args.withSubtasks) { return { success: true, data: { - tasks: sampleTasks.tasks, + tasks: tasksData, stats: { - total: sampleTasks.tasks.length, - completed: sampleTasks.tasks.filter((t) => t.status === 'done') + total: tasksData.length, + completed: tasksData.filter((t) => t.status === 'done').length, + inProgress: tasksData.filter((t) => t.status === 'in-progress') .length, - inProgress: sampleTasks.tasks.filter( - (t) => t.status === 'in-progress' - ).length, - pending: sampleTasks.tasks.filter((t) => t.status === 'pending') - .length + pending: tasksData.filter((t) => t.status === 'pending').length } }, fromCache: false @@ -258,16 +295,14 @@ describe('MCP Server Direct Functions', () => { // Status filter case if (args.status) { - const filteredTasks = sampleTasks.tasks.filter( - (t) => t.status === args.status - ); + const filteredTasks = tasksData.filter((t) => t.status === args.status); return { success: true, data: { tasks: filteredTasks, filter: args.status, stats: { - total: sampleTasks.tasks.length, + total: tasksData.length, filtered: filteredTasks.length } }, @@ -280,10 +315,10 @@ describe('MCP Server Direct Functions', () => { return { success: true, data: { - tasks: sampleTasks.tasks, + tasks: tasksData, includeSubtasks: true, stats: { - total: sampleTasks.tasks.length + total: tasksData.length } }, fromCache: false @@ -370,6 +405,29 @@ describe('MCP Server Direct Functions', () => { expect(result.error.code).toBe('FILE_NOT_FOUND_ERROR'); expect(mockLogger.error).toHaveBeenCalled(); }); + + test('should include complexity scores when complexity report exists', async () => { + // Arrange + mockReadComplexityReport.mockReturnValueOnce(mockComplexityReport); + const args = { + projectRoot: testProjectRoot, + file: testTasksPath, + withSubtasks: true + }; + + // Act + const result = await testListTasks(args, mockLogger); + // Assert + expect(result.success).toBe(true); + + // Check that tasks have complexity scores from the report + mockComplexityReport.complexityAnalysis.forEach((analysis) => { + const task = result.data.tasks.find((t) => t.id === analysis.taskId); + if (task) { + expect(task.complexityScore).toBe(analysis.complexityScore); + } + }); + }); }); describe('expandTaskDirect', () => { diff --git a/tests/unit/task-finder.test.js b/tests/unit/task-finder.test.js index 30cb9bc6..b480a2a2 100644 --- a/tests/unit/task-finder.test.js +++ b/tests/unit/task-finder.test.js @@ -2,8 +2,9 @@ * Task finder tests */ +// Import after mocks are set up - No mocks needed for readComplexityReport anymore import { findTaskById } from '../../scripts/modules/utils.js'; -import { sampleTasks, emptySampleTasks } from '../fixtures/sample-tasks.js'; +import { emptySampleTasks, sampleTasks } from '../fixtures/sample-tasks.js'; describe('Task Finder', () => { describe('findTaskById function', () => { @@ -55,5 +56,62 @@ describe('Task Finder', () => { expect(result.task).toBeNull(); expect(result.originalSubtaskCount).toBeNull(); }); + test('should work correctly when no complexity report is provided', () => { + // Pass null as the complexity report + const result = findTaskById(sampleTasks.tasks, 2, null); + + expect(result.task).toBeDefined(); + expect(result.task.id).toBe(2); + expect(result.task.complexityScore).toBeUndefined(); + }); + test('should work correctly when task has no complexity data in the provided report', () => { + // Define a complexity report that doesn't include task 2 + const complexityReport = { + complexityAnalysis: [{ taskId: 999, complexityScore: 5 }] + }; + + const result = findTaskById(sampleTasks.tasks, 2, complexityReport); + + expect(result.task).toBeDefined(); + expect(result.task.id).toBe(2); + expect(result.task.complexityScore).toBeUndefined(); + }); + + test('should include complexity score when report is provided', () => { + // Define the complexity report for this test + const complexityReport = { + meta: { + generatedAt: '2023-01-01T00:00:00.000Z', + tasksAnalyzed: 3, + thresholdScore: 5 + }, + complexityAnalysis: [ + { + taskId: 1, + taskTitle: 'Initialize Project', + complexityScore: 3, + recommendedSubtasks: 2 + }, + { + taskId: 2, + taskTitle: 'Create Core Functionality', + complexityScore: 8, + recommendedSubtasks: 5 + }, + { + taskId: 3, + taskTitle: 'Implement UI Components', + complexityScore: 6, + recommendedSubtasks: 4 + } + ] + }; + + const result = findTaskById(sampleTasks.tasks, 2, complexityReport); + + expect(result.task).toBeDefined(); + expect(result.task.id).toBe(2); + expect(result.task.complexityScore).toBe(8); + }); }); });