From 20004a39ea848f747e1ff48981bfe176554e4055 Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Fri, 3 Oct 2025 18:47:05 +0200 Subject: [PATCH] fix: add complexity score to tm list and tm show (#1270) --- .changeset/whole-pigs-say.md | 8 + apps/cli/src/commands/list.command.ts | 23 ++- .../src/ui/components/dashboard.component.ts | 3 +- .../src/ui/components/next-task.component.ts | 7 + .../ui/components/task-detail.component.ts | 10 +- apps/cli/src/utils/ui.ts | 45 ++++- packages/tm-core/src/entities/task.entity.ts | 11 +- packages/tm-core/src/index.ts | 9 + .../src/reports/complexity-report-manager.ts | 185 ++++++++++++++++++ packages/tm-core/src/reports/index.ts | 11 ++ packages/tm-core/src/reports/types.ts | 65 ++++++ packages/tm-core/src/services/task-service.ts | 10 - .../src/storage/file-storage/file-storage.ts | 49 ++++- packages/tm-core/src/types/index.ts | 9 +- 14 files changed, 411 insertions(+), 34 deletions(-) create mode 100644 .changeset/whole-pigs-say.md create mode 100644 packages/tm-core/src/reports/complexity-report-manager.ts create mode 100644 packages/tm-core/src/reports/index.ts create mode 100644 packages/tm-core/src/reports/types.ts diff --git a/.changeset/whole-pigs-say.md b/.changeset/whole-pigs-say.md new file mode 100644 index 00000000..eca334fe --- /dev/null +++ b/.changeset/whole-pigs-say.md @@ -0,0 +1,8 @@ +--- +"task-master-ai": patch +--- + +Fix complexity score not showing for `task-master show` and `task-master list` + +- Added complexity score on "next task" when running `task-master list` +- Added colors to complexity to reflect complexity (easy, medium, hard) diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index 57adf824..a49583b5 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -281,9 +281,14 @@ export class ListTasksCommand extends Command { const priorityBreakdown = getPriorityBreakdown(tasks); // Find next task following the same logic as findNextTask - const nextTask = this.findNextTask(tasks); + const nextTaskInfo = this.findNextTask(tasks); - // Display dashboard boxes + // Get the full task object with complexity data already included + const nextTask = nextTaskInfo + ? tasks.find((t) => String(t.id) === String(nextTaskInfo.id)) + : undefined; + + // Display dashboard boxes (nextTask already has complexity from storage enrichment) displayDashboards( taskStats, subtaskStats, @@ -303,14 +308,16 @@ export class ListTasksCommand extends Command { // Display recommended next task section immediately after table if (nextTask) { - // Find the full task object to get description - const fullTask = tasks.find((t) => String(t.id) === String(nextTask.id)); - const description = fullTask ? getTaskDescription(fullTask) : undefined; + const description = getTaskDescription(nextTask); displayRecommendedNextTask({ - ...nextTask, - status: 'pending', // Next task is typically pending - description + id: nextTask.id, + title: nextTask.title, + priority: nextTask.priority, + status: nextTask.status, + dependencies: nextTask.dependencies, + description, + complexity: nextTask.complexity as number | undefined }); } else { displayRecommendedNextTask(undefined); diff --git a/apps/cli/src/ui/components/dashboard.component.ts b/apps/cli/src/ui/components/dashboard.component.ts index 56ffeb70..2f21d0c2 100644 --- a/apps/cli/src/ui/components/dashboard.component.ts +++ b/apps/cli/src/ui/components/dashboard.component.ts @@ -6,6 +6,7 @@ import chalk from 'chalk'; import boxen from 'boxen'; import type { Task, TaskPriority } from '@tm/core/types'; +import { getComplexityWithColor } from '../../utils/ui.js'; /** * Statistics for task collection @@ -479,7 +480,7 @@ export function displayDependencyDashboard( ? chalk.cyan(nextTask.dependencies.join(', ')) : chalk.gray('None') }\n` + - `Complexity: ${nextTask?.complexity || chalk.gray('N/A')}`; + `Complexity: ${nextTask?.complexity !== undefined ? getComplexityWithColor(nextTask.complexity) : chalk.gray('N/A')}`; return content; } diff --git a/apps/cli/src/ui/components/next-task.component.ts b/apps/cli/src/ui/components/next-task.component.ts index beb5d7cc..91822a45 100644 --- a/apps/cli/src/ui/components/next-task.component.ts +++ b/apps/cli/src/ui/components/next-task.component.ts @@ -6,6 +6,7 @@ import chalk from 'chalk'; import boxen from 'boxen'; import type { Task } from '@tm/core/types'; +import { getComplexityWithColor } from '../../utils/ui.js'; /** * Next task display options @@ -17,6 +18,7 @@ export interface NextTaskDisplayOptions { status?: string; dependencies?: (string | number)[]; description?: string; + complexity?: number; } /** @@ -82,6 +84,11 @@ export function displayRecommendedNextTask( : chalk.cyan(task.dependencies.join(', ')); content.push(`Dependencies: ${depsDisplay}`); + // Complexity with color and label + if (typeof task.complexity === 'number') { + content.push(`Complexity: ${getComplexityWithColor(task.complexity)}`); + } + // Description if available if (task.description) { content.push(''); diff --git a/apps/cli/src/ui/components/task-detail.component.ts b/apps/cli/src/ui/components/task-detail.component.ts index 218d46dc..646d7616 100644 --- a/apps/cli/src/ui/components/task-detail.component.ts +++ b/apps/cli/src/ui/components/task-detail.component.ts @@ -9,7 +9,11 @@ import Table from 'cli-table3'; import { marked, MarkedExtension } from 'marked'; import { markedTerminal } from 'marked-terminal'; import type { Task } from '@tm/core/types'; -import { getStatusWithColor, getPriorityWithColor } from '../../utils/ui.js'; +import { + getStatusWithColor, + getPriorityWithColor, + getComplexityWithColor +} from '../../utils/ui.js'; // Configure marked to use terminal renderer with subtle colors marked.use( @@ -108,7 +112,9 @@ export function displayTaskProperties(task: Task): void { getStatusWithColor(task.status), getPriorityWithColor(task.priority), deps, - 'N/A', + typeof task.complexity === 'number' + ? getComplexityWithColor(task.complexity) + : chalk.gray('N/A'), task.description || '' ].join('\n'); diff --git a/apps/cli/src/utils/ui.ts b/apps/cli/src/utils/ui.ts index 8c8a7141..60626a8c 100644 --- a/apps/cli/src/utils/ui.ts +++ b/apps/cli/src/utils/ui.ts @@ -84,7 +84,23 @@ export function getPriorityWithColor(priority: TaskPriority): string { } /** - * Get colored complexity display + * Get complexity color and label based on score thresholds + */ +function getComplexityLevel(score: number): { + color: (text: string) => string; + label: string; +} { + if (score >= 7) { + return { color: chalk.hex('#CC0000'), label: 'High' }; + } else if (score >= 4) { + return { color: chalk.hex('#FF8800'), label: 'Medium' }; + } else { + return { color: chalk.green, label: 'Low' }; + } +} + +/** + * Get colored complexity display with dot indicator (simple format) */ export function getComplexityWithColor(complexity: number | string): string { const score = @@ -94,13 +110,20 @@ export function getComplexityWithColor(complexity: number | string): string { return chalk.gray('N/A'); } - if (score >= 8) { - return chalk.red.bold(`${score} (High)`); - } else if (score >= 5) { - return chalk.yellow(`${score} (Medium)`); - } else { - return chalk.green(`${score} (Low)`); + const { color } = getComplexityLevel(score); + return color(`● ${score}`); +} + +/** + * Get colored complexity display with /10 format (for dashboards) + */ +export function getComplexityWithScore(complexity: number | undefined): string { + if (typeof complexity !== 'number') { + return chalk.gray('N/A'); } + + const { color, label } = getComplexityLevel(complexity); + return color(`${complexity}/10 (${label})`); } /** @@ -323,8 +346,12 @@ export function createTaskTable( } if (showComplexity) { - // Show N/A if no complexity score - row.push(chalk.gray('N/A')); + // Show complexity score from report if available + if (typeof task.complexity === 'number') { + row.push(getComplexityWithColor(task.complexity)); + } else { + row.push(chalk.gray('N/A')); + } } table.push(row); diff --git a/packages/tm-core/src/entities/task.entity.ts b/packages/tm-core/src/entities/task.entity.ts index 724ab6b9..32403034 100644 --- a/packages/tm-core/src/entities/task.entity.ts +++ b/packages/tm-core/src/entities/task.entity.ts @@ -33,6 +33,9 @@ export class TaskEntity implements Task { tags?: string[]; assignee?: string; complexity?: Task['complexity']; + recommendedSubtasks?: number; + expansionPrompt?: string; + complexityReasoning?: string; constructor(data: Task | (Omit & { id: number | string })) { this.validate(data); @@ -62,6 +65,9 @@ export class TaskEntity implements Task { this.tags = data.tags; this.assignee = data.assignee; this.complexity = data.complexity; + this.recommendedSubtasks = data.recommendedSubtasks; + this.expansionPrompt = data.expansionPrompt; + this.complexityReasoning = data.complexityReasoning; } /** @@ -246,7 +252,10 @@ export class TaskEntity implements Task { actualEffort: this.actualEffort, tags: this.tags, assignee: this.assignee, - complexity: this.complexity + complexity: this.complexity, + recommendedSubtasks: this.recommendedSubtasks, + expansionPrompt: this.expansionPrompt, + complexityReasoning: this.complexityReasoning }; } diff --git a/packages/tm-core/src/index.ts b/packages/tm-core/src/index.ts index 274892f0..7702b868 100644 --- a/packages/tm-core/src/index.ts +++ b/packages/tm-core/src/index.ts @@ -61,3 +61,12 @@ export { getLogger, createLogger, setGlobalLogger } from './logger/index.js'; // Re-export executors export * from './executors/index.js'; + +// Re-export reports +export { + ComplexityReportManager, + type ComplexityReport, + type ComplexityReportMetadata, + type ComplexityAnalysis, + type TaskComplexityData +} from './reports/index.js'; diff --git a/packages/tm-core/src/reports/complexity-report-manager.ts b/packages/tm-core/src/reports/complexity-report-manager.ts new file mode 100644 index 00000000..e55758a5 --- /dev/null +++ b/packages/tm-core/src/reports/complexity-report-manager.ts @@ -0,0 +1,185 @@ +/** + * @fileoverview ComplexityReportManager - Handles loading and managing complexity analysis reports + * Follows the same pattern as ConfigManager and AuthManager + */ + +import { promises as fs } from 'fs'; +import path from 'path'; +import type { + ComplexityReport, + ComplexityAnalysis, + TaskComplexityData +} from './types.js'; +import { getLogger } from '../logger/index.js'; + +const logger = getLogger('ComplexityReportManager'); + +/** + * Manages complexity analysis reports + * Handles loading, caching, and providing complexity data for tasks + */ +export class ComplexityReportManager { + private projectRoot: string; + private reportCache: Map = new Map(); + + constructor(projectRoot: string) { + this.projectRoot = projectRoot; + } + + /** + * Get the path to the complexity report file for a given tag + */ + private getReportPath(tag?: string): string { + const reportsDir = path.join(this.projectRoot, '.taskmaster', 'reports'); + const tagSuffix = tag && tag !== 'master' ? `_${tag}` : ''; + return path.join(reportsDir, `task-complexity-report${tagSuffix}.json`); + } + + /** + * Load complexity report for a given tag + * Results are cached to avoid repeated file reads + */ + async loadReport(tag?: string): Promise { + const resolvedTag = tag || 'master'; + const cacheKey = resolvedTag; + + // Check cache first + if (this.reportCache.has(cacheKey)) { + return this.reportCache.get(cacheKey)!; + } + + const reportPath = this.getReportPath(tag); + + try { + // Check if file exists + await fs.access(reportPath); + + // Read and parse the report + const content = await fs.readFile(reportPath, 'utf-8'); + const report = JSON.parse(content) as ComplexityReport; + + // Validate basic structure + if (!report.meta || !Array.isArray(report.complexityAnalysis)) { + logger.warn( + `Invalid complexity report structure at ${reportPath}, ignoring` + ); + return null; + } + + // Cache the report + this.reportCache.set(cacheKey, report); + + logger.debug( + `Loaded complexity report for tag '${resolvedTag}' with ${report.complexityAnalysis.length} analyses` + ); + + return report; + } catch (error: any) { + if (error.code === 'ENOENT') { + // File doesn't exist - this is normal, not all projects have complexity reports + logger.debug(`No complexity report found for tag '${resolvedTag}'`); + return null; + } + + // Other errors (parsing, permissions, etc.) + logger.warn( + `Failed to load complexity report for tag '${resolvedTag}': ${error.message}` + ); + return null; + } + } + + /** + * Get complexity data for a specific task ID + */ + async getComplexityForTask( + taskId: string | number, + tag?: string + ): Promise { + const report = await this.loadReport(tag); + if (!report) { + return null; + } + + // Find the analysis for this task + const analysis = report.complexityAnalysis.find( + (a) => String(a.taskId) === String(taskId) + ); + + if (!analysis) { + return null; + } + + // Convert to TaskComplexityData format + return { + complexityScore: analysis.complexityScore, + recommendedSubtasks: analysis.recommendedSubtasks, + expansionPrompt: analysis.expansionPrompt, + complexityReasoning: analysis.complexityReasoning + }; + } + + /** + * Get complexity data for multiple tasks at once + * More efficient than calling getComplexityForTask multiple times + */ + async getComplexityForTasks( + taskIds: (string | number)[], + tag?: string + ): Promise> { + const result = new Map(); + const report = await this.loadReport(tag); + + if (!report) { + return result; + } + + // Create a map for fast lookups + const analysisMap = new Map(); + report.complexityAnalysis.forEach((analysis) => { + analysisMap.set(String(analysis.taskId), analysis); + }); + + // Map each task ID to its complexity data + taskIds.forEach((taskId) => { + const analysis = analysisMap.get(String(taskId)); + if (analysis) { + result.set(String(taskId), { + complexityScore: analysis.complexityScore, + recommendedSubtasks: analysis.recommendedSubtasks, + expansionPrompt: analysis.expansionPrompt, + complexityReasoning: analysis.complexityReasoning + }); + } + }); + + return result; + } + + /** + * Clear the report cache + * @param tag - Specific tag to clear, or undefined to clear all cached reports + * Useful when reports are regenerated or modified externally + */ + clearCache(tag?: string): void { + if (tag) { + this.reportCache.delete(tag); + } else { + // Clear all cached reports + this.reportCache.clear(); + } + } + + /** + * Check if a complexity report exists for a tag + */ + async hasReport(tag?: string): Promise { + const reportPath = this.getReportPath(tag); + try { + await fs.access(reportPath); + return true; + } catch { + return false; + } + } +} diff --git a/packages/tm-core/src/reports/index.ts b/packages/tm-core/src/reports/index.ts new file mode 100644 index 00000000..6f6e48cd --- /dev/null +++ b/packages/tm-core/src/reports/index.ts @@ -0,0 +1,11 @@ +/** + * @fileoverview Reports module exports + */ + +export { ComplexityReportManager } from './complexity-report-manager.js'; +export type { + ComplexityReport, + ComplexityReportMetadata, + ComplexityAnalysis, + TaskComplexityData +} from './types.js'; diff --git a/packages/tm-core/src/reports/types.ts b/packages/tm-core/src/reports/types.ts new file mode 100644 index 00000000..f5903c2c --- /dev/null +++ b/packages/tm-core/src/reports/types.ts @@ -0,0 +1,65 @@ +/** + * @fileoverview Type definitions for complexity analysis reports + */ + +/** + * Analysis result for a single task + */ +export interface ComplexityAnalysis { + /** Task ID being analyzed */ + taskId: string | number; + /** Task title */ + taskTitle: string; + /** Complexity score (1-10 scale) */ + complexityScore: number; + /** Recommended number of subtasks */ + recommendedSubtasks: number; + /** AI-generated prompt for task expansion */ + expansionPrompt: string; + /** Reasoning behind the complexity assessment */ + complexityReasoning: string; +} + +/** + * Metadata about the complexity report + */ +export interface ComplexityReportMetadata { + /** When the report was generated */ + generatedAt: string; + /** Number of tasks analyzed in this run */ + tasksAnalyzed: number; + /** Total number of tasks in the file */ + totalTasks?: number; + /** Total analyses in the report (across all runs) */ + analysisCount?: number; + /** Complexity threshold score used */ + thresholdScore: number; + /** Project name */ + projectName?: string; + /** Whether research mode was used */ + usedResearch: boolean; +} + +/** + * Complete complexity analysis report + */ +export interface ComplexityReport { + /** Report metadata */ + meta: ComplexityReportMetadata; + /** Array of complexity analyses */ + complexityAnalysis: ComplexityAnalysis[]; +} + +/** + * Complexity data to be attached to a Task + */ +export interface TaskComplexityData { + /** Complexity score (1-10 scale) */ + complexityScore?: number; + /** Recommended number of subtasks */ + recommendedSubtasks?: number; + /** AI-generated expansion prompt */ + expansionPrompt?: string; + /** Reasoning behind the assessment */ + complexityReasoning?: string; +} diff --git a/packages/tm-core/src/services/task-service.ts b/packages/tm-core/src/services/task-service.ts index 2b050e29..c0c01839 100644 --- a/packages/tm-core/src/services/task-service.ts +++ b/packages/tm-core/src/services/task-service.ts @@ -397,16 +397,6 @@ export class TaskService { } } - // Complexity filter - if (filter.complexity) { - const complexities = Array.isArray(filter.complexity) - ? filter.complexity - : [filter.complexity]; - if (!task.complexity || !complexities.includes(task.complexity)) { - return false; - } - } - // Search filter if (filter.search) { const searchLower = filter.search.toLowerCase(); diff --git a/packages/tm-core/src/storage/file-storage/file-storage.ts b/packages/tm-core/src/storage/file-storage/file-storage.ts index 14384de5..73749e70 100644 --- a/packages/tm-core/src/storage/file-storage/file-storage.ts +++ b/packages/tm-core/src/storage/file-storage/file-storage.ts @@ -11,6 +11,7 @@ import type { import { FormatHandler } from './format-handler.js'; import { FileOperations } from './file-operations.js'; import { PathResolver } from './path-resolver.js'; +import { ComplexityReportManager } from '../../reports/complexity-report-manager.js'; /** * File-based storage implementation using a single tasks.json file with separated concerns @@ -19,11 +20,13 @@ export class FileStorage implements IStorage { private formatHandler: FormatHandler; private fileOps: FileOperations; private pathResolver: PathResolver; + private complexityManager: ComplexityReportManager; constructor(projectPath: string) { this.formatHandler = new FormatHandler(); this.fileOps = new FileOperations(); this.pathResolver = new PathResolver(projectPath); + this.complexityManager = new ComplexityReportManager(projectPath); } /** @@ -87,6 +90,7 @@ export class FileStorage implements IStorage { /** * Load tasks from the single tasks.json file for a specific tag + * Enriches tasks with complexity data from the complexity report */ async loadTasks(tag?: string): Promise { const filePath = this.pathResolver.getTasksPath(); @@ -94,7 +98,10 @@ export class FileStorage implements IStorage { try { const rawData = await this.fileOps.readJson(filePath); - return this.formatHandler.extractTasks(rawData, resolvedTag); + const tasks = this.formatHandler.extractTasks(rawData, resolvedTag); + + // Enrich tasks with complexity data + return await this.enrichTasksWithComplexity(tasks, resolvedTag); } catch (error: any) { if (error.code === 'ENOENT') { return []; // File doesn't exist, return empty array @@ -596,6 +603,46 @@ export class FileStorage implements IStorage { await this.saveTasks(tasks, targetTag); } + + /** + * Enrich tasks with complexity data from the complexity report + * Private helper method called by loadTasks() + */ + private async enrichTasksWithComplexity( + tasks: Task[], + tag: string + ): Promise { + // Get all task IDs for bulk lookup + const taskIds = tasks.map((t) => t.id); + + // Load complexity data for all tasks at once (more efficient) + const complexityMap = await this.complexityManager.getComplexityForTasks( + taskIds, + tag + ); + + // If no complexity data found, return tasks as-is + if (complexityMap.size === 0) { + return tasks; + } + + // Enrich each task with its complexity data + return tasks.map((task) => { + const complexityData = complexityMap.get(String(task.id)); + if (!complexityData) { + return task; + } + + // Merge complexity data into the task + return { + ...task, + complexity: complexityData.complexityScore, + recommendedSubtasks: complexityData.recommendedSubtasks, + expansionPrompt: complexityData.expansionPrompt, + complexityReasoning: complexityData.complexityReasoning + }; + }); + } } // Export as default for convenience diff --git a/packages/tm-core/src/types/index.ts b/packages/tm-core/src/types/index.ts index 4befc21e..38d54084 100644 --- a/packages/tm-core/src/types/index.ts +++ b/packages/tm-core/src/types/index.ts @@ -72,7 +72,13 @@ export interface Task { actualEffort?: number; tags?: string[]; assignee?: string; - complexity?: TaskComplexity; + + // Complexity analysis (from complexity report) + // Can be either enum ('simple' | 'moderate' | 'complex' | 'very-complex') or numeric score (1-10) + complexity?: TaskComplexity | number; + recommendedSubtasks?: number; + expansionPrompt?: string; + complexityReasoning?: string; } /** @@ -145,7 +151,6 @@ export interface TaskFilter { hasSubtasks?: boolean; search?: string; assignee?: string; - complexity?: TaskComplexity | TaskComplexity[]; } /**