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 <shreypaharia@gmail.com>
This commit is contained in:
Ralph Khreish
2025-05-16 23:24:25 +02:00
committed by GitHub
parent a8dabf4485
commit 58b417a8ce
16 changed files with 490 additions and 74 deletions

View File

@@ -1072,10 +1072,16 @@ function registerCommands(programInstance) {
.command('list')
.description('List all tasks')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-r, --report <report>',
'Path to the complexity report file',
'scripts/task-complexity-report.json'
)
.option('-s, --status <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 <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-r, --report <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 <id>', 'Task ID to show')
.option('-s, --status <status>', 'Filter subtasks by status') // ADDED status option
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-r, --report <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

View File

@@ -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
};

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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]
);

View File

@@ -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
};