refactor: enhance add-task fuzzy search and fix duplicate banner display

- **Remove hardcoded category system** in add-task that always matched 'Task management'
- **Eliminate arbitrary limits** in fuzzy search results (5→25 high relevance, 3→10 medium relevance, 8→20 detailed tasks)
- **Improve semantic weighting** in Fuse.js search (details=3, description=2, title=1.5) for better relevance
- **Fix duplicate banner issue** by removing console.clear() and redundant displayBanner() calls from UI functions
- **Enhance context generation** to rely on semantic similarity rather than rigid pattern matching
- **Preserve terminal history** to address GitHub issue #553 about eating terminal lines
- **Remove displayBanner() calls** from: displayHelp, displayNextTask, displayTaskById, displayComplexityReport, set-task-status, clear-subtasks, dependency-manager functions

The add-task system now provides truly relevant task context based on semantic similarity rather than arbitrary categories and limits, while maintaining a cleaner terminal experience.

Changes span: add-task.js, ui.js, set-task-status.js, clear-subtasks.js, list-tasks.js, dependency-manager.js

Closes #553
This commit is contained in:
Eyal Toledano
2025-06-07 20:23:55 -04:00
parent 54005d5486
commit af652978a0
9 changed files with 3047 additions and 3095 deletions

View File

@@ -1,8 +1,8 @@
{
"models": {
"main": {
"provider": "openrouter",
"modelId": "qwen/qwen3-235b-a22b:free",
"provider": "anthropic",
"modelId": "claude-sonnet-4-20250514",
"maxTokens": 50000,
"temperature": 0.2
},

View File

@@ -1,35 +0,0 @@
# Task ID: 97
# Title: Create Taskmaster Jingle Implementation
# Status: pending
# Dependencies: 95, 57, 3, 2
# Priority: medium
# Description: Develop a musical jingle system for Taskmaster that plays sound effects during key CLI interactions to enhance user experience.
# Details:
This task involves implementing a sound system that plays audio cues during Taskmaster CLI operations. Key implementation steps include:
1. Audio System Integration:
- Research and select appropriate audio library compatible with Node.js CLI applications
- Implement cross-platform audio playback (Windows, macOS, Linux)
- Create sound configuration options in .taskmasterconfig
2. Jingle Design:
- Define sound triggers for key events (task creation, completion, errors, etc.)
- Create or source appropriate sound files (WAV/MP3 format)
- Implement volume control and mute option in settings
3. CLI Integration:
- Add sound playback to core CLI commands (init, create, update, delete)
- Implement optional sound effects toggle via command line flags
- Ensure audio playback doesn't interfere with CLI performance
4. Documentation:
- Update user guide with sound configuration instructions
- Add troubleshooting section for audio playback issues
# Test Strategy:
1. Verify audio plays correctly during each supported CLI operation
2. Test sound configuration options across different platforms
3. Confirm volume control and mute functionality works as expected
4. Validate that audio playback doesn't affect CLI performance
5. Test edge cases (no audio hardware, invalid sound files, etc.)
6. Ensure sound effects can be disabled via configuration and CLI flags

View File

@@ -5871,22 +5871,6 @@
"parentTaskId": 96
}
]
},
{
"id": 97,
"title": "Create Taskmaster Jingle Implementation",
"description": "Develop a musical jingle system for Taskmaster that plays sound effects during key CLI interactions to enhance user experience.",
"details": "This task involves implementing a sound system that plays audio cues during Taskmaster CLI operations. Key implementation steps include:\n\n1. Audio System Integration:\n - Research and select appropriate audio library compatible with Node.js CLI applications\n - Implement cross-platform audio playback (Windows, macOS, Linux)\n - Create sound configuration options in .taskmasterconfig\n\n2. Jingle Design:\n - Define sound triggers for key events (task creation, completion, errors, etc.)\n - Create or source appropriate sound files (WAV/MP3 format)\n - Implement volume control and mute option in settings\n\n3. CLI Integration:\n - Add sound playback to core CLI commands (init, create, update, delete)\n - Implement optional sound effects toggle via command line flags\n - Ensure audio playback doesn't interfere with CLI performance\n\n4. Documentation:\n - Update user guide with sound configuration instructions\n - Add troubleshooting section for audio playback issues",
"testStrategy": "1. Verify audio plays correctly during each supported CLI operation\n2. Test sound configuration options across different platforms\n3. Confirm volume control and mute functionality works as expected\n4. Validate that audio playback doesn't affect CLI performance\n5. Test edge cases (no audio hardware, invalid sound files, etc.)\n6. Ensure sound effects can be disabled via configuration and CLI flags",
"status": "pending",
"dependencies": [
95,
57,
3,
2
],
"priority": "medium",
"subtasks": []
}
]
}

View File

@@ -3,9 +3,9 @@
* Manages task dependencies and relationships
*/
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import path from "path";
import chalk from "chalk";
import boxen from "boxen";
import {
log,
@@ -14,12 +14,12 @@ import {
taskExists,
formatTaskId,
findCycles,
isSilentMode
} from './utils.js';
isSilentMode,
} from "./utils.js";
import { displayBanner } from './ui.js';
import { displayBanner } from "./ui.js";
import { generateTaskFiles } from './task-manager.js';
import { generateTaskFiles } from "./task-manager.js";
/**
* Add a dependency to a task
@@ -28,17 +28,17 @@ import { generateTaskFiles } from './task-manager.js';
* @param {number|string} dependencyId - ID of the task to add as dependency
*/
async function addDependency(tasksPath, taskId, dependencyId) {
log('info', `Adding dependency ${dependencyId} to task ${taskId}...`);
log("info", `Adding dependency ${dependencyId} to task ${taskId}...`);
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log('error', 'No valid tasks found in tasks.json');
log("error", "No valid tasks found in tasks.json");
process.exit(1);
}
// Format the task and dependency IDs correctly
const formattedTaskId =
typeof taskId === 'string' && taskId.includes('.')
typeof taskId === "string" && taskId.includes(".")
? taskId
: parseInt(taskId, 10);
@@ -47,7 +47,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
// Check if the dependency task or subtask actually exists
if (!taskExists(data.tasks, formattedDependencyId)) {
log(
'error',
"error",
`Dependency target ${formattedDependencyId} does not exist in tasks.json`
);
process.exit(1);
@@ -57,20 +57,20 @@ async function addDependency(tasksPath, taskId, dependencyId) {
let targetTask = null;
let isSubtask = false;
if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) {
if (typeof formattedTaskId === "string" && formattedTaskId.includes(".")) {
// Handle dot notation for subtasks (e.g., "1.2")
const [parentId, subtaskId] = formattedTaskId
.split('.')
.split(".")
.map((id) => parseInt(id, 10));
const parentTask = data.tasks.find((t) => t.id === parentId);
if (!parentTask) {
log('error', `Parent task ${parentId} not found.`);
log("error", `Parent task ${parentId} not found.`);
process.exit(1);
}
if (!parentTask.subtasks) {
log('error', `Parent task ${parentId} has no subtasks.`);
log("error", `Parent task ${parentId} has no subtasks.`);
process.exit(1);
}
@@ -78,7 +78,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
isSubtask = true;
if (!targetTask) {
log('error', `Subtask ${formattedTaskId} not found.`);
log("error", `Subtask ${formattedTaskId} not found.`);
process.exit(1);
}
} else {
@@ -86,7 +86,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
targetTask = data.tasks.find((t) => t.id === formattedTaskId);
if (!targetTask) {
log('error', `Task ${formattedTaskId} not found.`);
log("error", `Task ${formattedTaskId} not found.`);
process.exit(1);
}
}
@@ -104,7 +104,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
})
) {
log(
'warn',
"warn",
`Dependency ${formattedDependencyId} already exists in task ${formattedTaskId}.`
);
return;
@@ -112,7 +112,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
// Check if the task is trying to depend on itself - compare full IDs (including subtask parts)
if (String(formattedTaskId) === String(formattedDependencyId)) {
log('error', `Task ${formattedTaskId} cannot depend on itself.`);
log("error", `Task ${formattedTaskId} cannot depend on itself.`);
process.exit(1);
}
@@ -121,30 +121,30 @@ async function addDependency(tasksPath, taskId, dependencyId) {
let isSelfDependency = false;
if (
typeof formattedTaskId === 'string' &&
typeof formattedDependencyId === 'string' &&
formattedTaskId.includes('.') &&
formattedDependencyId.includes('.')
typeof formattedTaskId === "string" &&
typeof formattedDependencyId === "string" &&
formattedTaskId.includes(".") &&
formattedDependencyId.includes(".")
) {
const [taskParentId] = formattedTaskId.split('.');
const [depParentId] = formattedDependencyId.split('.');
const [taskParentId] = formattedTaskId.split(".");
const [depParentId] = formattedDependencyId.split(".");
// Only treat it as a self-dependency if both the parent ID and subtask ID are identical
isSelfDependency = formattedTaskId === formattedDependencyId;
// Log for debugging
log(
'debug',
"debug",
`Adding dependency between subtasks: ${formattedTaskId} depends on ${formattedDependencyId}`
);
log(
'debug',
"debug",
`Parent IDs: ${taskParentId} and ${depParentId}, Self-dependency check: ${isSelfDependency}`
);
}
if (isSelfDependency) {
log('error', `Subtask ${formattedTaskId} cannot depend on itself.`);
log("error", `Subtask ${formattedTaskId} cannot depend on itself.`);
process.exit(1);
}
@@ -158,13 +158,13 @@ async function addDependency(tasksPath, taskId, dependencyId) {
// Sort dependencies numerically or by parent task ID first, then subtask ID
targetTask.dependencies.sort((a, b) => {
if (typeof a === 'number' && typeof b === 'number') {
if (typeof a === "number" && typeof b === "number") {
return a - b;
} else if (typeof a === 'string' && typeof b === 'string') {
const [aParent, aChild] = a.split('.').map(Number);
const [bParent, bChild] = b.split('.').map(Number);
} else if (typeof a === "string" && typeof b === "string") {
const [aParent, aChild] = a.split(".").map(Number);
const [bParent, bChild] = b.split(".").map(Number);
return aParent !== bParent ? aParent - bParent : aChild - bChild;
} else if (typeof a === 'number') {
} else if (typeof a === "number") {
return -1; // Numbers come before strings
} else {
return 1; // Strings come after numbers
@@ -174,7 +174,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
// Save changes
writeJSON(tasksPath, data);
log(
'success',
"success",
`Added dependency ${formattedDependencyId} to task ${formattedTaskId}`
);
@@ -186,9 +186,9 @@ async function addDependency(tasksPath, taskId, dependencyId) {
`Task ${chalk.bold(formattedTaskId)} now depends on ${chalk.bold(formattedDependencyId)}`,
{
padding: 1,
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1 }
borderColor: "green",
borderStyle: "round",
margin: { top: 1 },
}
)
);
@@ -197,10 +197,10 @@ async function addDependency(tasksPath, taskId, dependencyId) {
// Generate updated task files
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
log('info', 'Task files regenerated with updated dependencies.');
log("info", "Task files regenerated with updated dependencies.");
} else {
log(
'error',
"error",
`Cannot add dependency ${formattedDependencyId} to task ${formattedTaskId} as it would create a circular dependency.`
);
process.exit(1);
@@ -214,18 +214,18 @@ async function addDependency(tasksPath, taskId, dependencyId) {
* @param {number|string} dependencyId - ID of the task to remove as dependency
*/
async function removeDependency(tasksPath, taskId, dependencyId) {
log('info', `Removing dependency ${dependencyId} from task ${taskId}...`);
log("info", `Removing dependency ${dependencyId} from task ${taskId}...`);
// Read tasks file
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log('error', 'No valid tasks found.');
log("error", "No valid tasks found.");
process.exit(1);
}
// Format the task and dependency IDs correctly
const formattedTaskId =
typeof taskId === 'string' && taskId.includes('.')
typeof taskId === "string" && taskId.includes(".")
? taskId
: parseInt(taskId, 10);
@@ -235,20 +235,20 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
let targetTask = null;
let isSubtask = false;
if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) {
if (typeof formattedTaskId === "string" && formattedTaskId.includes(".")) {
// Handle dot notation for subtasks (e.g., "1.2")
const [parentId, subtaskId] = formattedTaskId
.split('.')
.split(".")
.map((id) => parseInt(id, 10));
const parentTask = data.tasks.find((t) => t.id === parentId);
if (!parentTask) {
log('error', `Parent task ${parentId} not found.`);
log("error", `Parent task ${parentId} not found.`);
process.exit(1);
}
if (!parentTask.subtasks) {
log('error', `Parent task ${parentId} has no subtasks.`);
log("error", `Parent task ${parentId} has no subtasks.`);
process.exit(1);
}
@@ -256,7 +256,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
isSubtask = true;
if (!targetTask) {
log('error', `Subtask ${formattedTaskId} not found.`);
log("error", `Subtask ${formattedTaskId} not found.`);
process.exit(1);
}
} else {
@@ -264,7 +264,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
targetTask = data.tasks.find((t) => t.id === formattedTaskId);
if (!targetTask) {
log('error', `Task ${formattedTaskId} not found.`);
log("error", `Task ${formattedTaskId} not found.`);
process.exit(1);
}
}
@@ -272,7 +272,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
// Check if the task has any dependencies
if (!targetTask.dependencies || targetTask.dependencies.length === 0) {
log(
'info',
"info",
`Task ${formattedTaskId} has no dependencies, nothing to remove.`
);
return;
@@ -287,10 +287,10 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
let depStr = String(dep);
// Special handling for numeric IDs that might be subtask references
if (typeof dep === 'number' && dep < 100 && isSubtask) {
if (typeof dep === "number" && dep < 100 && isSubtask) {
// It's likely a reference to another subtask in the same parent task
// Convert to full format for comparison (e.g., 2 -> "1.2" for a subtask in task 1)
const [parentId] = formattedTaskId.split('.');
const [parentId] = formattedTaskId.split(".");
depStr = `${parentId}.${dep}`;
}
@@ -299,7 +299,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
if (dependencyIndex === -1) {
log(
'info',
"info",
`Task ${formattedTaskId} does not depend on ${formattedDependencyId}, no changes made.`
);
return;
@@ -313,7 +313,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
// Success message
log(
'success',
"success",
`Removed dependency: Task ${formattedTaskId} no longer depends on ${formattedDependencyId}`
);
@@ -325,9 +325,9 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
`Task ${chalk.bold(formattedTaskId)} no longer depends on ${chalk.bold(formattedDependencyId)}`,
{
padding: 1,
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1 }
borderColor: "green",
borderStyle: "round",
margin: { top: 1 },
}
)
);
@@ -358,8 +358,8 @@ function isCircularDependency(tasks, taskId, chain = []) {
let parentIdForSubtask = null;
// Check if this is a subtask reference (e.g., "1.2")
if (taskIdStr.includes('.')) {
const [parentId, subtaskId] = taskIdStr.split('.').map(Number);
if (taskIdStr.includes(".")) {
const [parentId, subtaskId] = taskIdStr.split(".").map(Number);
const parentTask = tasks.find((t) => t.id === parentId);
parentIdForSubtask = parentId; // Store parent ID if it's a subtask
@@ -385,7 +385,7 @@ function isCircularDependency(tasks, taskId, chain = []) {
return task.dependencies.some((depId) => {
let normalizedDepId = String(depId);
// Normalize relative subtask dependencies
if (typeof depId === 'number' && parentIdForSubtask !== null) {
if (typeof depId === "number" && parentIdForSubtask !== null) {
// If the current task is a subtask AND the dependency is a number,
// assume it refers to a sibling subtask.
normalizedDepId = `${parentIdForSubtask}.${depId}`;
@@ -413,9 +413,9 @@ function validateTaskDependencies(tasks) {
// Check for self-dependencies
if (String(depId) === String(task.id)) {
issues.push({
type: 'self',
type: "self",
taskId: task.id,
message: `Task ${task.id} depends on itself`
message: `Task ${task.id} depends on itself`,
});
return;
}
@@ -423,10 +423,10 @@ function validateTaskDependencies(tasks) {
// Check if dependency exists
if (!taskExists(tasks, depId)) {
issues.push({
type: 'missing',
type: "missing",
taskId: task.id,
dependencyId: depId,
message: `Task ${task.id} depends on non-existent task ${depId}`
message: `Task ${task.id} depends on non-existent task ${depId}`,
});
}
});
@@ -434,9 +434,9 @@ function validateTaskDependencies(tasks) {
// Check for circular dependencies
if (isCircularDependency(tasks, task.id)) {
issues.push({
type: 'circular',
type: "circular",
taskId: task.id,
message: `Task ${task.id} is part of a circular dependency chain`
message: `Task ${task.id} is part of a circular dependency chain`,
});
}
@@ -454,12 +454,12 @@ function validateTaskDependencies(tasks) {
// Check for self-dependencies in subtasks
if (
String(depId) === String(fullSubtaskId) ||
(typeof depId === 'number' && depId === subtask.id)
(typeof depId === "number" && depId === subtask.id)
) {
issues.push({
type: 'self',
type: "self",
taskId: fullSubtaskId,
message: `Subtask ${fullSubtaskId} depends on itself`
message: `Subtask ${fullSubtaskId} depends on itself`,
});
return;
}
@@ -467,10 +467,10 @@ function validateTaskDependencies(tasks) {
// Check if dependency exists
if (!taskExists(tasks, depId)) {
issues.push({
type: 'missing',
type: "missing",
taskId: fullSubtaskId,
dependencyId: depId,
message: `Subtask ${fullSubtaskId} depends on non-existent task/subtask ${depId}`
message: `Subtask ${fullSubtaskId} depends on non-existent task/subtask ${depId}`,
});
}
});
@@ -478,9 +478,9 @@ function validateTaskDependencies(tasks) {
// Check for circular dependencies in subtasks
if (isCircularDependency(tasks, fullSubtaskId)) {
issues.push({
type: 'circular',
type: "circular",
taskId: fullSubtaskId,
message: `Subtask ${fullSubtaskId} is part of a circular dependency chain`
message: `Subtask ${fullSubtaskId} is part of a circular dependency chain`,
});
}
});
@@ -489,7 +489,7 @@ function validateTaskDependencies(tasks) {
return {
valid: issues.length === 0,
issues
issues,
};
}
@@ -508,13 +508,13 @@ function removeDuplicateDependencies(tasksData) {
const uniqueDeps = [...new Set(task.dependencies)];
return {
...task,
dependencies: uniqueDeps
dependencies: uniqueDeps,
};
});
return {
...tasksData,
tasks
tasks,
};
}
@@ -554,7 +554,7 @@ function cleanupSubtaskDependencies(tasksData) {
return {
...tasksData,
tasks
tasks,
};
}
@@ -563,17 +563,12 @@ function cleanupSubtaskDependencies(tasksData) {
* @param {string} tasksPath - Path to tasks.json
*/
async function validateDependenciesCommand(tasksPath, options = {}) {
// Only display banner if not in silent mode
if (!isSilentMode()) {
displayBanner();
}
log('info', 'Checking for invalid dependencies in task files...');
log("info", "Checking for invalid dependencies in task files...");
// Read tasks data
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log('error', 'No valid tasks found in tasks.json');
log("error", "No valid tasks found in tasks.json");
process.exit(1);
}
@@ -587,7 +582,7 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
});
log(
'info',
"info",
`Analyzing dependencies for ${taskCount} tasks and ${subtaskCount} subtasks...`
);
@@ -597,7 +592,7 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
if (!validationResult.valid) {
log(
'error',
"error",
`Dependency validation failed. Found ${validationResult.issues.length} issue(s):`
);
validationResult.issues.forEach((issue) => {
@@ -605,7 +600,7 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
if (issue.dependencyId) {
errorMsg += ` (Dependency: ${issue.dependencyId})`;
}
log('error', errorMsg); // Log each issue as an error
log("error", errorMsg); // Log each issue as an error
});
// Optionally exit if validation fails, depending on desired behavior
@@ -616,22 +611,22 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
console.log(
boxen(
chalk.red(`Dependency Validation FAILED\n\n`) +
`${chalk.cyan('Tasks checked:')} ${taskCount}\n` +
`${chalk.cyan('Subtasks checked:')} ${subtaskCount}\n` +
`${chalk.red('Issues found:')} ${validationResult.issues.length}`, // Display count from result
`${chalk.cyan("Tasks checked:")} ${taskCount}\n` +
`${chalk.cyan("Subtasks checked:")} ${subtaskCount}\n` +
`${chalk.red("Issues found:")} ${validationResult.issues.length}`, // Display count from result
{
padding: 1,
borderColor: 'red',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
borderColor: "red",
borderStyle: "round",
margin: { top: 1, bottom: 1 },
}
)
);
}
} else {
log(
'success',
'No invalid dependencies found - all dependencies are valid'
"success",
"No invalid dependencies found - all dependencies are valid"
);
// Show validation summary - only if not in silent mode
@@ -639,21 +634,21 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
console.log(
boxen(
chalk.green(`All Dependencies Are Valid\n\n`) +
`${chalk.cyan('Tasks checked:')} ${taskCount}\n` +
`${chalk.cyan('Subtasks checked:')} ${subtaskCount}\n` +
`${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`,
`${chalk.cyan("Tasks checked:")} ${taskCount}\n` +
`${chalk.cyan("Subtasks checked:")} ${subtaskCount}\n` +
`${chalk.cyan("Total dependencies verified:")} ${countAllDependencies(data.tasks)}`,
{
padding: 1,
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
borderColor: "green",
borderStyle: "round",
margin: { top: 1, bottom: 1 },
}
)
);
}
}
} catch (error) {
log('error', 'Error validating dependencies:', error);
log("error", "Error validating dependencies:", error);
process.exit(1);
}
}
@@ -691,18 +686,13 @@ function countAllDependencies(tasks) {
* @param {Object} options - Options object
*/
async function fixDependenciesCommand(tasksPath, options = {}) {
// Only display banner if not in silent mode
if (!isSilentMode()) {
displayBanner();
}
log('info', 'Checking for and fixing invalid dependencies in tasks.json...');
log("info", "Checking for and fixing invalid dependencies in tasks.json...");
try {
// Read tasks data
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log('error', 'No valid tasks found in tasks.json');
log("error", "No valid tasks found in tasks.json");
process.exit(1);
}
@@ -716,7 +706,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
duplicateDependenciesRemoved: 0,
circularDependenciesFixed: 0,
tasksFixed: 0,
subtasksFixed: 0
subtasksFixed: 0,
};
// First phase: Remove duplicate dependencies in tasks
@@ -728,7 +718,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
const depIdStr = String(depId);
if (uniqueDeps.has(depIdStr)) {
log(
'info',
"info",
`Removing duplicate dependency from task ${task.id}: ${depId}`
);
stats.duplicateDependenciesRemoved++;
@@ -750,12 +740,12 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
const originalLength = subtask.dependencies.length;
subtask.dependencies = subtask.dependencies.filter((depId) => {
let depIdStr = String(depId);
if (typeof depId === 'number' && depId < 100) {
if (typeof depId === "number" && depId < 100) {
depIdStr = `${task.id}.${depId}`;
}
if (uniqueDeps.has(depIdStr)) {
log(
'info',
"info",
`Removing duplicate dependency from subtask ${task.id}.${subtask.id}: ${depId}`
);
stats.duplicateDependenciesRemoved++;
@@ -788,13 +778,13 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (task.dependencies && Array.isArray(task.dependencies)) {
const originalLength = task.dependencies.length;
task.dependencies = task.dependencies.filter((depId) => {
const isSubtask = typeof depId === 'string' && depId.includes('.');
const isSubtask = typeof depId === "string" && depId.includes(".");
if (isSubtask) {
// Check if the subtask exists
if (!validSubtaskIds.has(depId)) {
log(
'info',
"info",
`Removing invalid subtask dependency from task ${task.id}: ${depId} (subtask does not exist)`
);
stats.nonExistentDependenciesRemoved++;
@@ -804,10 +794,10 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
} else {
// Check if the task exists
const numericId =
typeof depId === 'string' ? parseInt(depId, 10) : depId;
typeof depId === "string" ? parseInt(depId, 10) : depId;
if (!validTaskIds.has(numericId)) {
log(
'info',
"info",
`Removing invalid task dependency from task ${task.id}: ${depId} (task does not exist)`
);
stats.nonExistentDependenciesRemoved++;
@@ -831,9 +821,9 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
// First check for self-dependencies
const hasSelfDependency = subtask.dependencies.some((depId) => {
if (typeof depId === 'string' && depId.includes('.')) {
if (typeof depId === "string" && depId.includes(".")) {
return depId === subtaskId;
} else if (typeof depId === 'number' && depId < 100) {
} else if (typeof depId === "number" && depId < 100) {
return depId === subtask.id;
}
return false;
@@ -842,13 +832,13 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (hasSelfDependency) {
subtask.dependencies = subtask.dependencies.filter((depId) => {
const normalizedDepId =
typeof depId === 'number' && depId < 100
typeof depId === "number" && depId < 100
? `${task.id}.${depId}`
: String(depId);
if (normalizedDepId === subtaskId) {
log(
'info',
"info",
`Removing self-dependency from subtask ${subtaskId}`
);
stats.selfDependenciesRemoved++;
@@ -860,10 +850,10 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
// Then check for non-existent dependencies
subtask.dependencies = subtask.dependencies.filter((depId) => {
if (typeof depId === 'string' && depId.includes('.')) {
if (typeof depId === "string" && depId.includes(".")) {
if (!validSubtaskIds.has(depId)) {
log(
'info',
"info",
`Removing invalid subtask dependency from subtask ${subtaskId}: ${depId} (subtask does not exist)`
);
stats.nonExistentDependenciesRemoved++;
@@ -874,7 +864,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
// Handle numeric dependencies
const numericId =
typeof depId === 'number' ? depId : parseInt(depId, 10);
typeof depId === "number" ? depId : parseInt(depId, 10);
// Small numbers likely refer to subtasks in the same task
if (numericId < 100) {
@@ -882,7 +872,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (!validSubtaskIds.has(fullSubtaskId)) {
log(
'info',
"info",
`Removing invalid subtask dependency from subtask ${subtaskId}: ${numericId}`
);
stats.nonExistentDependenciesRemoved++;
@@ -895,7 +885,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
// Otherwise it's a task reference
if (!validTaskIds.has(numericId)) {
log(
'info',
"info",
`Removing invalid task dependency from subtask ${subtaskId}: ${numericId}`
);
stats.nonExistentDependenciesRemoved++;
@@ -914,7 +904,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
});
// Third phase: Check for circular dependencies
log('info', 'Checking for circular dependencies...');
log("info", "Checking for circular dependencies...");
// Build the dependency map for subtasks
const subtaskDependencyMap = new Map();
@@ -925,9 +915,9 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
const normalizedDeps = subtask.dependencies.map((depId) => {
if (typeof depId === 'string' && depId.includes('.')) {
if (typeof depId === "string" && depId.includes(".")) {
return depId;
} else if (typeof depId === 'number' && depId < 100) {
} else if (typeof depId === "number" && depId < 100) {
return `${task.id}.${depId}`;
}
return String(depId);
@@ -955,7 +945,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (cycleEdges.length > 0) {
const [taskId, subtaskNum] = subtaskId
.split('.')
.split(".")
.map((part) => Number(part));
const task = data.tasks.find((t) => t.id === taskId);
@@ -966,9 +956,9 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
const originalLength = subtask.dependencies.length;
const edgesToRemove = cycleEdges.map((edge) => {
if (edge.includes('.')) {
if (edge.includes(".")) {
const [depTaskId, depSubtaskId] = edge
.split('.')
.split(".")
.map((part) => Number(part));
if (depTaskId === taskId) {
@@ -983,7 +973,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
subtask.dependencies = subtask.dependencies.filter((depId) => {
const normalizedDepId =
typeof depId === 'number' && depId < 100
typeof depId === "number" && depId < 100
? `${taskId}.${depId}`
: String(depId);
@@ -992,7 +982,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
edgesToRemove.includes(normalizedDepId)
) {
log(
'info',
"info",
`Breaking circular dependency: Removing ${normalizedDepId} from subtask ${subtaskId}`
);
stats.circularDependenciesFixed++;
@@ -1015,13 +1005,13 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (dataChanged) {
// Save the changes
writeJSON(tasksPath, data);
log('success', 'Fixed dependency issues in tasks.json');
log("success", "Fixed dependency issues in tasks.json");
// Regenerate task files
log('info', 'Regenerating task files to reflect dependency changes...');
log("info", "Regenerating task files to reflect dependency changes...");
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
} else {
log('info', 'No changes needed to fix dependencies');
log("info", "No changes needed to fix dependencies");
}
// Show detailed statistics report
@@ -1033,48 +1023,48 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (!isSilentMode()) {
if (totalFixedAll > 0) {
log('success', `Fixed ${totalFixedAll} dependency issues in total!`);
log("success", `Fixed ${totalFixedAll} dependency issues in total!`);
console.log(
boxen(
chalk.green(`Dependency Fixes Summary:\n\n`) +
`${chalk.cyan('Invalid dependencies removed:')} ${stats.nonExistentDependenciesRemoved}\n` +
`${chalk.cyan('Self-dependencies removed:')} ${stats.selfDependenciesRemoved}\n` +
`${chalk.cyan('Duplicate dependencies removed:')} ${stats.duplicateDependenciesRemoved}\n` +
`${chalk.cyan('Circular dependencies fixed:')} ${stats.circularDependenciesFixed}\n\n` +
`${chalk.cyan('Tasks fixed:')} ${stats.tasksFixed}\n` +
`${chalk.cyan('Subtasks fixed:')} ${stats.subtasksFixed}\n`,
`${chalk.cyan("Invalid dependencies removed:")} ${stats.nonExistentDependenciesRemoved}\n` +
`${chalk.cyan("Self-dependencies removed:")} ${stats.selfDependenciesRemoved}\n` +
`${chalk.cyan("Duplicate dependencies removed:")} ${stats.duplicateDependenciesRemoved}\n` +
`${chalk.cyan("Circular dependencies fixed:")} ${stats.circularDependenciesFixed}\n\n` +
`${chalk.cyan("Tasks fixed:")} ${stats.tasksFixed}\n` +
`${chalk.cyan("Subtasks fixed:")} ${stats.subtasksFixed}\n`,
{
padding: 1,
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
borderColor: "green",
borderStyle: "round",
margin: { top: 1, bottom: 1 },
}
)
);
} else {
log(
'success',
'No dependency issues found - all dependencies are valid'
"success",
"No dependency issues found - all dependencies are valid"
);
console.log(
boxen(
chalk.green(`All Dependencies Are Valid\n\n`) +
`${chalk.cyan('Tasks checked:')} ${data.tasks.length}\n` +
`${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`,
`${chalk.cyan("Tasks checked:")} ${data.tasks.length}\n` +
`${chalk.cyan("Total dependencies verified:")} ${countAllDependencies(data.tasks)}`,
{
padding: 1,
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
borderColor: "green",
borderStyle: "round",
margin: { top: 1, bottom: 1 },
}
)
);
}
}
} catch (error) {
log('error', 'Error in fix-dependencies command:', error);
log("error", "Error in fix-dependencies command:", error);
process.exit(1);
}
}
@@ -1113,7 +1103,7 @@ function ensureAtLeastOneIndependentSubtask(tasksData) {
if (task.subtasks.length > 0) {
const firstSubtask = task.subtasks[0];
log(
'debug',
"debug",
`Ensuring at least one independent subtask: Clearing dependencies for subtask ${task.id}.${firstSubtask.id}`
);
firstSubtask.dependencies = [];
@@ -1134,11 +1124,11 @@ function ensureAtLeastOneIndependentSubtask(tasksData) {
*/
function validateAndFixDependencies(tasksData, tasksPath = null) {
if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) {
log('error', 'Invalid tasks data');
log("error", "Invalid tasks data");
return false;
}
log('debug', 'Validating and fixing dependencies...');
log("debug", "Validating and fixing dependencies...");
// Create a deep copy for comparison
const originalData = JSON.parse(JSON.stringify(tasksData));
@@ -1184,7 +1174,7 @@ function validateAndFixDependencies(tasksData, tasksPath = null) {
if (subtask.dependencies) {
subtask.dependencies = subtask.dependencies.filter((depId) => {
// Handle numeric subtask references
if (typeof depId === 'number' && depId < 100) {
if (typeof depId === "number" && depId < 100) {
const fullSubtaskId = `${task.id}.${depId}`;
return taskExists(tasksData.tasks, fullSubtaskId);
}
@@ -1220,9 +1210,9 @@ function validateAndFixDependencies(tasksData, tasksPath = null) {
if (tasksPath && changesDetected) {
try {
writeJSON(tasksPath, tasksData);
log('debug', 'Saved dependency fixes to tasks.json');
log("debug", "Saved dependency fixes to tasks.json");
} catch (error) {
log('error', 'Failed to save dependency fixes to tasks.json', error);
log("error", "Failed to save dependency fixes to tasks.json", error);
}
}
@@ -1239,5 +1229,5 @@ export {
removeDuplicateDependencies,
cleanupSubtaskDependencies,
ensureAtLeastOneIndependentSubtask,
validateAndFixDependencies
validateAndFixDependencies,
};

View File

@@ -1,40 +1,42 @@
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';
import { z } from 'zod';
import Fuse from 'fuse.js'; // Import Fuse.js for advanced fuzzy search
import path from "path";
import chalk from "chalk";
import boxen from "boxen";
import Table from "cli-table3";
import { z } from "zod";
import Fuse from "fuse.js"; // Import Fuse.js for advanced fuzzy search
import {
displayBanner,
getStatusWithColor,
startLoadingIndicator,
stopLoadingIndicator,
displayAiUsageSummary
} from '../ui.js';
import { readJSON, writeJSON, log as consoleLog, truncate } from '../utils.js';
import { generateObjectService } from '../ai-services-unified.js';
import { getDefaultPriority } from '../config-manager.js';
import generateTaskFiles from './generate-task-files.js';
succeedLoadingIndicator,
failLoadingIndicator,
displayAiUsageSummary,
} from "../ui.js";
import { readJSON, writeJSON, log as consoleLog, truncate } from "../utils.js";
import { generateObjectService } from "../ai-services-unified.js";
import { getDefaultPriority } from "../config-manager.js";
import generateTaskFiles from "./generate-task-files.js";
// Define Zod schema for the expected AI output object
const AiTaskDataSchema = z.object({
title: z.string().describe('Clear, concise title for the task'),
title: z.string().describe("Clear, concise title for the task"),
description: z
.string()
.describe('A one or two sentence description of the task'),
.describe("A one or two sentence description of the task"),
details: z
.string()
.describe('In-depth implementation details, considerations, and guidance'),
.describe("In-depth implementation details, considerations, and guidance"),
testStrategy: z
.string()
.describe('Detailed approach for verifying task completion'),
.describe("Detailed approach for verifying task completion"),
dependencies: z
.array(z.number())
.optional()
.describe(
'Array of task IDs that this task depends on (must be completed before this task can start)'
)
"Array of task IDs that this task depends on (must be completed before this task can start)"
),
});
/**
@@ -62,7 +64,7 @@ async function addTask(
dependencies = [],
priority = null,
context = {},
outputFormat = 'text', // Default to text for CLI
outputFormat = "text", // Default to text for CLI
manualTaskData = null,
useResearch = false
) {
@@ -74,27 +76,27 @@ async function addTask(
? mcpLog // Use MCP logger if provided
: {
// Create a wrapper around consoleLog for CLI
info: (...args) => consoleLog('info', ...args),
warn: (...args) => consoleLog('warn', ...args),
error: (...args) => consoleLog('error', ...args),
debug: (...args) => consoleLog('debug', ...args),
success: (...args) => consoleLog('success', ...args)
info: (...args) => consoleLog("info", ...args),
warn: (...args) => consoleLog("warn", ...args),
error: (...args) => consoleLog("error", ...args),
debug: (...args) => consoleLog("debug", ...args),
success: (...args) => consoleLog("success", ...args),
};
const effectivePriority = priority || getDefaultPriority(projectRoot);
logFn.info(
`Adding new task with prompt: "${prompt}", Priority: ${effectivePriority}, Dependencies: ${dependencies.join(', ') || 'None'}, Research: ${useResearch}, ProjectRoot: ${projectRoot}`
`Adding new task with prompt: "${prompt}", Priority: ${effectivePriority}, Dependencies: ${dependencies.join(", ") || "None"}, Research: ${useResearch}, ProjectRoot: ${projectRoot}`
);
let loadingIndicator = null;
let aiServiceResponse = null; // To store the full response from AI service
// Create custom reporter that checks for MCP log
const report = (message, level = 'info') => {
const report = (message, level = "info") => {
if (mcpLog) {
mcpLog[level](message);
} else if (outputFormat === 'text') {
} else if (outputFormat === "text") {
consoleLog(level, message);
}
};
@@ -156,7 +158,7 @@ async function addTask(
title: task.title,
description: task.description,
status: task.status,
dependencies: dependencyData
dependencies: dependencyData,
};
}
@@ -166,14 +168,14 @@ async function addTask(
// If tasks.json doesn't exist or is invalid, create a new one
if (!data || !data.tasks) {
report('tasks.json not found or invalid. Creating a new one.', 'info');
report("tasks.json not found or invalid. Creating a new one.", "info");
// Create default tasks data structure
data = {
tasks: []
tasks: [],
};
// Ensure the directory exists and write the new file
writeJSON(tasksPath, data);
report('Created new tasks.json file with empty tasks array.', 'info');
report("Created new tasks.json file with empty tasks array.", "info");
}
// Find the highest task ID to determine the next ID
@@ -182,13 +184,13 @@ async function addTask(
const newTaskId = highestId + 1;
// Only show UI box for CLI mode
if (outputFormat === 'text') {
if (outputFormat === "text") {
console.log(
boxen(chalk.white.bold(`Creating New Task #${newTaskId}`), {
padding: 1,
borderColor: 'blue',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
borderColor: "blue",
borderStyle: "round",
margin: { top: 1, bottom: 1 },
})
);
}
@@ -202,10 +204,10 @@ async function addTask(
if (invalidDeps.length > 0) {
report(
`The following dependencies do not exist or are invalid: ${invalidDeps.join(', ')}`,
'warn'
`The following dependencies do not exist or are invalid: ${invalidDeps.join(", ")}`,
"warn"
);
report('Removing invalid dependencies...', 'info');
report("Removing invalid dependencies...", "info");
dependencies = dependencies.filter(
(depId) => !invalidDeps.includes(depId)
);
@@ -240,28 +242,28 @@ async function addTask(
// Check if manual task data is provided
if (manualTaskData) {
report('Using manually provided task data', 'info');
report("Using manually provided task data", "info");
taskData = manualTaskData;
report('DEBUG: Taking MANUAL task data path.', 'debug');
report("DEBUG: Taking MANUAL task data path.", "debug");
// Basic validation for manual data
if (
!taskData.title ||
typeof taskData.title !== 'string' ||
typeof taskData.title !== "string" ||
!taskData.description ||
typeof taskData.description !== 'string'
typeof taskData.description !== "string"
) {
throw new Error(
'Manual task data must include at least a title and description.'
"Manual task data must include at least a title and description."
);
}
} else {
report('DEBUG: Taking AI task generation path.', 'debug');
report("DEBUG: Taking AI task generation path.", "debug");
// --- Refactored AI Interaction ---
report(`Generating task data with AI with prompt:\n${prompt}`, 'info');
report(`Generating task data with AI with prompt:\n${prompt}`, "info");
// Create context string for task creation prompt
let contextTasks = '';
let contextTasks = "";
// Create a dependency map for better understanding of the task relationships
const taskMap = {};
@@ -272,18 +274,18 @@ async function addTask(
title: t.title,
description: t.description,
dependencies: t.dependencies || [],
status: t.status
status: t.status,
};
});
// CLI-only feedback for the dependency analysis
if (outputFormat === 'text') {
if (outputFormat === "text") {
console.log(
boxen(chalk.cyan.bold('Task Context Analysis') + '\n', {
boxen(chalk.cyan.bold("Task Context Analysis"), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
margin: { top: 0, bottom: 0 },
borderColor: 'cyan',
borderStyle: 'round'
borderColor: "cyan",
borderStyle: "round",
})
);
}
@@ -314,7 +316,7 @@ async function addTask(
const directDeps = data.tasks.filter((t) =>
numericDependencies.includes(t.id)
);
contextTasks += `\n${directDeps.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`).join('\n')}`;
contextTasks += `\n${directDeps.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`).join("\n")}`;
// Add an overview of indirect dependencies if present
const indirectDeps = dependentTasks.filter(
@@ -325,7 +327,7 @@ async function addTask(
contextTasks += `\n${indirectDeps
.slice(0, 5)
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
.join('\n')}`;
.join("\n")}`;
if (indirectDeps.length > 5) {
contextTasks += `\n- ... and ${indirectDeps.length - 5} more indirect dependencies`;
}
@@ -336,15 +338,15 @@ async function addTask(
for (const depTask of uniqueDetailedTasks) {
const depthInfo = depthMap.get(depTask.id)
? ` (depth: ${depthMap.get(depTask.id)})`
: '';
: "";
const isDirect = numericDependencies.includes(depTask.id)
? ' [DIRECT DEPENDENCY]'
: '';
? " [DIRECT DEPENDENCY]"
: "";
contextTasks += `\n\n------ Task ${depTask.id}${isDirect}${depthInfo}: ${depTask.title} ------\n`;
contextTasks += `Description: ${depTask.description}\n`;
contextTasks += `Status: ${depTask.status || 'pending'}\n`;
contextTasks += `Priority: ${depTask.priority || 'medium'}\n`;
contextTasks += `Status: ${depTask.status || "pending"}\n`;
contextTasks += `Priority: ${depTask.priority || "medium"}\n`;
// List its dependencies
if (depTask.dependencies && depTask.dependencies.length > 0) {
@@ -354,7 +356,7 @@ async function addTask(
? `Task ${dId}: ${depDepTask.title}`
: `Task ${dId}`;
});
contextTasks += `Dependencies: ${depDeps.join(', ')}\n`;
contextTasks += `Dependencies: ${depDeps.join(", ")}\n`;
} else {
contextTasks += `Dependencies: None\n`;
}
@@ -363,7 +365,7 @@ async function addTask(
if (depTask.details) {
const truncatedDetails =
depTask.details.length > 400
? depTask.details.substring(0, 400) + '... (truncated)'
? depTask.details.substring(0, 400) + "... (truncated)"
: depTask.details;
contextTasks += `Implementation Details: ${truncatedDetails}\n`;
}
@@ -371,19 +373,19 @@ async function addTask(
// Add dependency chain visualization
if (dependencyGraphs.length > 0) {
contextTasks += '\n\nDependency Chain Visualization:';
contextTasks += "\n\nDependency Chain Visualization:";
// Helper function to format dependency chain as text
function formatDependencyChain(
node,
prefix = '',
prefix = "",
isLast = true,
depth = 0
) {
if (depth > 3) return ''; // Limit depth to avoid excessive nesting
if (depth > 3) return ""; // Limit depth to avoid excessive nesting
const connector = isLast ? '└── ' : '├── ';
const childPrefix = isLast ? ' ' : '';
const connector = isLast ? "└── " : "├── ";
const childPrefix = isLast ? " " : "";
let result = `\n${prefix}${connector}Task ${node.id}: ${node.title}`;
@@ -409,7 +411,7 @@ async function addTask(
}
// Show dependency analysis in CLI mode
if (outputFormat === 'text') {
if (outputFormat === "text") {
if (directDeps.length > 0) {
console.log(chalk.gray(` Explicitly specified dependencies:`));
directDeps.forEach((t) => {
@@ -449,14 +451,14 @@ async function addTask(
// Convert dependency graph to ASCII art for terminal
function visualizeDependencyGraph(
node,
prefix = '',
prefix = "",
isLast = true,
depth = 0
) {
if (depth > 2) return; // Limit depth for display
const connector = isLast ? '└── ' : '├── ';
const childPrefix = isLast ? ' ' : '';
const connector = isLast ? "└── " : "├── ";
const childPrefix = isLast ? " " : "";
console.log(
chalk.blue(
@@ -492,18 +494,18 @@ async function addTask(
includeScore: true, // Return match scores
threshold: 0.4, // Lower threshold = stricter matching (range 0-1)
keys: [
{ name: 'title', weight: 2 }, // Title is most important
{ name: 'description', weight: 1.5 }, // Description is next
{ name: 'details', weight: 0.8 }, // Details is less important
{ name: "title", weight: 1.5 }, // Title is most important
{ name: "description", weight: 2 }, // Description is very important
{ name: "details", weight: 3 }, // Details is most important
// Search dependencies to find tasks that depend on similar things
{ name: 'dependencyTitles', weight: 0.5 }
{ name: "dependencyTitles", weight: 0.5 },
],
// Sort matches by score (lower is better)
shouldSort: true,
// Allow searching in nested properties
useExtendedSearch: true,
// Return up to 15 matches
limit: 15
// Return up to 50 matches
limit: 50,
};
// Prepare task data with dependencies expanded as titles for better semantic search
@@ -514,15 +516,15 @@ async function addTask(
? task.dependencies
.map((depId) => {
const depTask = data.tasks.find((t) => t.id === depId);
return depTask ? depTask.title : '';
return depTask ? depTask.title : "";
})
.filter((title) => title)
.join(' ')
: '';
.join(" ")
: "";
return {
...task,
dependencyTitles
dependencyTitles,
};
});
@@ -532,7 +534,7 @@ async function addTask(
// Extract significant words and phrases from the prompt
const promptWords = prompt
.toLowerCase()
.replace(/[^\w\s-]/g, ' ') // Replace non-alphanumeric chars with spaces
.replace(/[^\w\s-]/g, " ") // Replace non-alphanumeric chars with spaces
.split(/\s+/)
.filter((word) => word.length > 3); // Words at least 4 chars
@@ -596,75 +598,40 @@ async function addTask(
// Get top N results for context
const relatedTasks = allRelevantTasks.slice(0, 8);
// Also look for tasks with similar purposes or categories
const purposeCategories = [
{ pattern: /(command|cli|flag)/i, label: 'CLI commands' },
{ pattern: /(task|subtask|add)/i, label: 'Task management' },
{ pattern: /(dependency|depend)/i, label: 'Dependency handling' },
{ pattern: /(AI|model|prompt)/i, label: 'AI integration' },
{ pattern: /(UI|display|show)/i, label: 'User interface' },
{ pattern: /(schedule|time|cron)/i, label: 'Scheduling' }, // Added scheduling category
{ pattern: /(config|setting|option)/i, label: 'Configuration' } // Added configuration category
];
promptCategory = purposeCategories.find((cat) =>
cat.pattern.test(prompt)
);
const categoryTasks = promptCategory
? data.tasks
.filter(
(t) =>
promptCategory.pattern.test(t.title) ||
promptCategory.pattern.test(t.description) ||
(t.details && promptCategory.pattern.test(t.details))
)
.filter((t) => !relatedTasks.some((rt) => rt.id === t.id))
.slice(0, 3)
: [];
// Format basic task overviews
if (relatedTasks.length > 0) {
contextTasks = `\nRelevant tasks identified by semantic similarity:\n${relatedTasks
.map((t, i) => {
const relevanceMarker = i < highRelevance.length ? '' : '';
const relevanceMarker = i < highRelevance.length ? "" : "";
return `- ${relevanceMarker}Task ${t.id}: ${t.title} - ${t.description}`;
})
.join('\n')}`;
}
if (categoryTasks.length > 0) {
contextTasks += `\n\nTasks related to ${promptCategory.label}:\n${categoryTasks
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
.join('\n')}`;
.join("\n")}`;
}
if (
recentTasks.length > 0 &&
!contextTasks.includes('Recently created tasks')
!contextTasks.includes("Recently created tasks")
) {
contextTasks += `\n\nRecently created tasks:\n${recentTasks
.filter((t) => !relatedTasks.some((rt) => rt.id === t.id))
.slice(0, 3)
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
.join('\n')}`;
.join("\n")}`;
}
// Add detailed information about the most relevant tasks
const allDetailedTasks = [
...relatedTasks.slice(0, 5),
...categoryTasks.slice(0, 2)
];
const allDetailedTasks = [...relatedTasks.slice(0, 25)];
uniqueDetailedTasks = Array.from(
new Map(allDetailedTasks.map((t) => [t.id, t])).values()
).slice(0, 8);
).slice(0, 20);
if (uniqueDetailedTasks.length > 0) {
contextTasks += `\n\nDetailed information about relevant tasks:`;
for (const task of uniqueDetailedTasks) {
contextTasks += `\n\n------ Task ${task.id}: ${task.title} ------\n`;
contextTasks += `Description: ${task.description}\n`;
contextTasks += `Status: ${task.status || 'pending'}\n`;
contextTasks += `Priority: ${task.priority || 'medium'}\n`;
contextTasks += `Status: ${task.status || "pending"}\n`;
contextTasks += `Priority: ${task.priority || "medium"}\n`;
if (task.dependencies && task.dependencies.length > 0) {
// Format dependency list with titles
const depList = task.dependencies.map((depId) => {
@@ -673,13 +640,13 @@ async function addTask(
? `Task ${depId} (${depTask.title})`
: `Task ${depId}`;
});
contextTasks += `Dependencies: ${depList.join(', ')}\n`;
contextTasks += `Dependencies: ${depList.join(", ")}\n`;
}
// Add implementation details but truncate if too long
if (task.details) {
const truncatedDetails =
task.details.length > 400
? task.details.substring(0, 400) + '... (truncated)'
? task.details.substring(0, 400) + "... (truncated)"
: task.details;
contextTasks += `Implementation Details: ${truncatedDetails}\n`;
}
@@ -687,7 +654,7 @@ async function addTask(
}
// Add a concise view of the task dependency structure
contextTasks += '\n\nSummary of task dependencies in the project:';
contextTasks += "\n\nSummary of task dependencies in the project:";
// Get pending/in-progress tasks that might be most relevant based on fuzzy search
// Prioritize tasks from our similarity search
@@ -695,7 +662,7 @@ async function addTask(
const relevantPendingTasks = data.tasks
.filter(
(t) =>
(t.status === 'pending' || t.status === 'in-progress') &&
(t.status === "pending" || t.status === "in-progress") &&
// Either in our relevant set OR has relevant words in title/description
(relevantTaskIds.has(t.id) ||
promptWords.some(
@@ -709,24 +676,20 @@ async function addTask(
for (const task of relevantPendingTasks) {
const depsStr =
task.dependencies && task.dependencies.length > 0
? task.dependencies.join(', ')
: 'None';
? task.dependencies.join(", ")
: "None";
contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`;
}
// Additional analysis of common patterns
const similarPurposeTasks = promptCategory
? data.tasks.filter(
(t) =>
promptCategory.pattern.test(t.title) ||
promptCategory.pattern.test(t.description)
)
: [];
const similarPurposeTasks = data.tasks.filter((t) =>
prompt.toLowerCase().includes(t.title.toLowerCase())
);
let commonDeps = []; // Initialize commonDeps
if (similarPurposeTasks.length > 0) {
contextTasks += `\n\nCommon patterns for ${promptCategory ? promptCategory.label : 'similar'} tasks:`;
contextTasks += `\n\nCommon patterns for similar tasks:`;
// Collect dependencies from similar purpose tasks
const similarDeps = similarPurposeTasks
@@ -743,10 +706,10 @@ async function addTask(
// Get most common dependencies for similar tasks
commonDeps = Object.entries(depCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
.slice(0, 10);
if (commonDeps.length > 0) {
contextTasks += '\nMost common dependencies for similar tasks:';
contextTasks += "\nMost common dependencies for similar tasks:";
commonDeps.forEach(([depId, count]) => {
const depTask = data.tasks.find((t) => t.id === parseInt(depId));
if (depTask) {
@@ -757,10 +720,10 @@ async function addTask(
}
// Show fuzzy search analysis in CLI mode
if (outputFormat === 'text') {
if (outputFormat === "text") {
console.log(
chalk.gray(
` Fuzzy search across ${data.tasks.length} tasks using full prompt and ${promptWords.length} keywords`
` Context search across ${data.tasks.length} tasks using full prompt and ${promptWords.length} keywords`
)
);
@@ -768,7 +731,7 @@ async function addTask(
console.log(
chalk.gray(`\n High relevance matches (score < 0.25):`)
);
highRelevance.slice(0, 5).forEach((t) => {
highRelevance.slice(0, 25).forEach((t) => {
console.log(
chalk.yellow(` • ⭐ Task ${t.id}: ${truncate(t.title, 50)}`)
);
@@ -779,24 +742,13 @@ async function addTask(
console.log(
chalk.gray(`\n Medium relevance matches (score < 0.4):`)
);
mediumRelevance.slice(0, 3).forEach((t) => {
mediumRelevance.slice(0, 10).forEach((t) => {
console.log(
chalk.green(` • Task ${t.id}: ${truncate(t.title, 50)}`)
);
});
}
if (promptCategory && categoryTasks.length > 0) {
console.log(
chalk.gray(`\n Tasks related to ${promptCategory.label}:`)
);
categoryTasks.forEach((t) => {
console.log(
chalk.magenta(` • Task ${t.id}: ${truncate(t.title, 50)}`)
);
});
}
// Show dependency patterns
if (commonDeps && commonDeps.length > 0) {
console.log(
@@ -825,7 +777,7 @@ async function addTask(
const isHighRelevance = highRelevance.some(
(ht) => ht.id === t.id
);
const relevanceIndicator = isHighRelevance ? '' : '';
const relevanceIndicator = isHighRelevance ? "" : "";
console.log(
chalk.cyan(
`${relevanceIndicator}Task ${t.id}: ${truncate(t.title, 40)}`
@@ -853,26 +805,23 @@ async function addTask(
}
// Add a visual transition to show we're moving to AI generation - only for CLI
if (outputFormat === 'text') {
if (outputFormat === "text") {
console.log(
boxen(
chalk.white.bold('AI Task Generation') +
`\n\n${chalk.gray('Analyzing context and generating task details using AI...')}` +
`\n${chalk.cyan('Context size: ')}${chalk.yellow(contextTasks.length.toLocaleString())} characters` +
`\n${chalk.cyan('Dependency detection: ')}${chalk.yellow(numericDependencies.length > 0 ? 'Explicit dependencies' : 'Auto-discovery mode')}` +
`\n${chalk.cyan('Detailed tasks: ')}${chalk.yellow(
chalk.white.bold("AI Task Generation") +
`\n\n${chalk.gray("Analyzing context and generating task details using AI...")}` +
`\n${chalk.cyan("Context size: ")}${chalk.yellow(contextTasks.length.toLocaleString())} characters` +
`\n${chalk.cyan("Dependency detection: ")}${chalk.yellow(numericDependencies.length > 0 ? "Explicit dependencies" : "Auto-discovery mode")}` +
`\n${chalk.cyan("Detailed tasks: ")}${chalk.yellow(
numericDependencies.length > 0
? dependentTasks.length // Use length of tasks from explicit dependency path
: uniqueDetailedTasks.length // Use length of tasks from fuzzy search path
)}` +
(promptCategory
? `\n${chalk.cyan('Category detected: ')}${chalk.yellow(promptCategory.label)}`
: ''),
)}`,
{
padding: { top: 0, bottom: 1, left: 1, right: 1 },
margin: { top: 1, bottom: 0 },
borderColor: 'white',
borderStyle: 'round'
borderColor: "white",
borderStyle: "round",
}
)
);
@@ -882,15 +831,15 @@ async function addTask(
// System Prompt - Enhanced for dependency awareness
const systemPrompt =
"You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\n" +
'When determining dependencies for a new task, follow these principles:\n' +
'1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n' +
'2. Prioritize task dependencies that are semantically related to the functionality being built.\n' +
'3. Consider both direct dependencies (immediately prerequisite) and indirect dependencies.\n' +
'4. Avoid adding unnecessary dependencies - only include tasks that are genuinely prerequisite.\n' +
'5. Consider the current status of tasks - prefer completed tasks as dependencies when possible.\n' +
"When determining dependencies for a new task, follow these principles:\n" +
"1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n" +
"2. Prioritize task dependencies that are semantically related to the functionality being built.\n" +
"3. Consider both direct dependencies (immediately prerequisite) and indirect dependencies.\n" +
"4. Avoid adding unnecessary dependencies - only include tasks that are genuinely prerequisite.\n" +
"5. Consider the current status of tasks - prefer completed tasks as dependencies when possible.\n" +
"6. Pay special attention to foundation tasks (1-5) but don't automatically include them without reason.\n" +
'7. Recent tasks (higher ID numbers) may be more relevant for newer functionality.\n\n' +
'The dependencies array should contain task IDs (numbers) of prerequisite tasks.\n';
"7. Recent tasks (higher ID numbers) may be more relevant for newer functionality.\n\n" +
"The dependencies array should contain task IDs (numbers) of prerequisite tasks.\n";
// Task Structure Description (for user prompt)
const taskStructureDesc = `
@@ -904,7 +853,7 @@ async function addTask(
`;
// Add any manually provided details to the prompt for context
let contextFromArgs = '';
let contextFromArgs = "";
if (manualTaskData?.title)
contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`;
if (manualTaskData?.description)
@@ -918,7 +867,7 @@ async function addTask(
const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project.
${contextTasks}
${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ''}
${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ""}
Based on the information about existing tasks provided above, include appropriate dependencies in the "dependencies" array. Only include task IDs that this new task directly depends on.
@@ -929,15 +878,15 @@ async function addTask(
`;
// Start the loading indicator - only for text mode
if (outputFormat === 'text') {
if (outputFormat === "text") {
loadingIndicator = startLoadingIndicator(
`Generating new task with ${useResearch ? 'Research' : 'Main'} AI...\n`
`Generating new task with ${useResearch ? "Research" : "Main"} AI... \n`
);
}
try {
const serviceRole = useResearch ? 'research' : 'main';
report('DEBUG: Calling generateObjectService...', 'debug');
const serviceRole = useResearch ? "research" : "main";
report("DEBUG: Calling generateObjectService...", "debug");
aiServiceResponse = await generateObjectService({
// Capture the full response
@@ -945,17 +894,17 @@ async function addTask(
session: session,
projectRoot: projectRoot,
schema: AiTaskDataSchema,
objectName: 'newTaskData',
objectName: "newTaskData",
systemPrompt: systemPrompt,
prompt: userPrompt,
commandName: commandName || 'add-task', // Use passed commandName or default
outputType: outputType || (isMCP ? 'mcp' : 'cli') // Use passed outputType or derive
commandName: commandName || "add-task", // Use passed commandName or default
outputType: outputType || (isMCP ? "mcp" : "cli"), // Use passed outputType or derive
});
report('DEBUG: generateObjectService returned successfully.', 'debug');
report("DEBUG: generateObjectService returned successfully.", "debug");
if (!aiServiceResponse || !aiServiceResponse.mainResult) {
throw new Error(
'AI service did not return the expected object structure.'
"AI service did not return the expected object structure."
);
}
@@ -972,21 +921,37 @@ async function addTask(
) {
taskData = aiServiceResponse.mainResult.object;
} else {
throw new Error('AI service did not return a valid task object.');
throw new Error("AI service did not return a valid task object.");
}
report('Successfully generated task data from AI.', 'success');
report("Successfully generated task data from AI.", "success");
// Success! Show checkmark
if (loadingIndicator) {
succeedLoadingIndicator(
loadingIndicator,
"Task generated successfully"
);
loadingIndicator = null; // Clear it
}
} catch (error) {
// Failure! Show X
if (loadingIndicator) {
failLoadingIndicator(loadingIndicator, "AI generation failed");
loadingIndicator = null;
}
report(
`DEBUG: generateObjectService caught error: ${error.message}`,
'debug'
"debug"
);
report(`Error generating task with AI: ${error.message}`, 'error');
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
report(`Error generating task with AI: ${error.message}`, "error");
throw error; // Re-throw error after logging
} finally {
report('DEBUG: generateObjectService finally block reached.', 'debug');
if (loadingIndicator) stopLoadingIndicator(loadingIndicator); // Ensure indicator stops
report("DEBUG: generateObjectService finally block reached.", "debug");
// Clean up if somehow still running
if (loadingIndicator) {
stopLoadingIndicator(loadingIndicator);
}
}
// --- End Refactored AI Interaction ---
}
@@ -996,14 +961,14 @@ async function addTask(
id: newTaskId,
title: taskData.title,
description: taskData.description,
details: taskData.details || '',
testStrategy: taskData.testStrategy || '',
status: 'pending',
details: taskData.details || "",
testStrategy: taskData.testStrategy || "",
status: "pending",
dependencies: taskData.dependencies?.length
? taskData.dependencies
: numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified
priority: effectivePriority,
subtasks: [] // Initialize with empty subtasks array
subtasks: [], // Initialize with empty subtasks array
};
// Additional check: validate all dependencies in the AI response
@@ -1015,8 +980,8 @@ async function addTask(
if (!allValidDeps) {
report(
'AI suggested invalid dependencies. Filtering them out...',
'warn'
"AI suggested invalid dependencies. Filtering them out...",
"warn"
);
newTask.dependencies = taskData.dependencies.filter((depId) => {
const numDepId = parseInt(depId, 10);
@@ -1028,48 +993,48 @@ async function addTask(
// Add the task to the tasks array
data.tasks.push(newTask);
report('DEBUG: Writing tasks.json...', 'debug');
report("DEBUG: Writing tasks.json...", "debug");
// Write the updated tasks to the file
writeJSON(tasksPath, data);
report('DEBUG: tasks.json written.', 'debug');
report("DEBUG: tasks.json written.", "debug");
// Generate markdown task files
report('Generating task files...', 'info');
report('DEBUG: Calling generateTaskFiles...', 'debug');
report("Generating task files...", "info");
report("DEBUG: Calling generateTaskFiles...", "debug");
// Pass mcpLog if available to generateTaskFiles
await generateTaskFiles(tasksPath, path.dirname(tasksPath), { mcpLog });
report('DEBUG: generateTaskFiles finished.', 'debug');
report("DEBUG: generateTaskFiles finished.", "debug");
// Show success message - only for text output (CLI)
if (outputFormat === 'text') {
if (outputFormat === "text") {
const table = new Table({
head: [
chalk.cyan.bold('ID'),
chalk.cyan.bold('Title'),
chalk.cyan.bold('Description')
chalk.cyan.bold("ID"),
chalk.cyan.bold("Title"),
chalk.cyan.bold("Description"),
],
colWidths: [5, 30, 50] // Adjust widths as needed
colWidths: [5, 30, 50], // Adjust widths as needed
});
table.push([
newTask.id,
truncate(newTask.title, 27),
truncate(newTask.description, 47)
truncate(newTask.description, 47),
]);
console.log(chalk.green('✅ New task created successfully:'));
console.log(chalk.green("✓ New task created successfully:"));
console.log(table.toString());
// Helper to get priority color
const getPriorityColor = (p) => {
switch (p?.toLowerCase()) {
case 'high':
return 'red';
case 'low':
return 'gray';
case 'medium':
case "high":
return "red";
case "low":
return "gray";
case "medium":
default:
return 'yellow';
return "yellow";
}
};
@@ -1093,49 +1058,49 @@ async function addTask(
});
// Prepare dependency display string
let dependencyDisplay = '';
let dependencyDisplay = "";
if (newTask.dependencies.length > 0) {
dependencyDisplay = chalk.white('Dependencies:') + '\n';
dependencyDisplay = chalk.white("Dependencies:") + "\n";
newTask.dependencies.forEach((dep) => {
const isAiAdded = aiAddedDeps.includes(dep);
const depType = isAiAdded ? chalk.yellow(' (AI suggested)') : '';
const depType = isAiAdded ? chalk.yellow(" (AI suggested)") : "";
dependencyDisplay +=
chalk.white(
` - ${dep}: ${depTitles[dep] || 'Unknown task'}${depType}`
) + '\n';
` - ${dep}: ${depTitles[dep] || "Unknown task"}${depType}`
) + "\n";
});
} else {
dependencyDisplay = chalk.white('Dependencies: None') + '\n';
dependencyDisplay = chalk.white("Dependencies: None") + "\n";
}
// Add info about removed dependencies if any
if (aiRemovedDeps.length > 0) {
dependencyDisplay +=
chalk.gray('\nUser-specified dependencies that were not used:') +
'\n';
chalk.gray("\nUser-specified dependencies that were not used:") +
"\n";
aiRemovedDeps.forEach((dep) => {
const depTask = data.tasks.find((t) => t.id === dep);
const title = depTask ? truncate(depTask.title, 30) : 'Unknown task';
dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + '\n';
const title = depTask ? truncate(depTask.title, 30) : "Unknown task";
dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + "\n";
});
}
// Add dependency analysis summary
let dependencyAnalysis = '';
let dependencyAnalysis = "";
if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) {
dependencyAnalysis =
'\n' + chalk.white.bold('Dependency Analysis:') + '\n';
"\n" + chalk.white.bold("Dependency Analysis:") + "\n";
if (aiAddedDeps.length > 0) {
dependencyAnalysis +=
chalk.green(
`AI identified ${aiAddedDeps.length} additional dependencies`
) + '\n';
) + "\n";
}
if (aiRemovedDeps.length > 0) {
dependencyAnalysis +=
chalk.yellow(
`AI excluded ${aiRemovedDeps.length} user-provided dependencies`
) + '\n';
) + "\n";
}
}
@@ -1143,32 +1108,32 @@ async function addTask(
console.log(
boxen(
chalk.white.bold(`Task ${newTaskId} Created Successfully`) +
'\n\n' +
"\n\n" +
chalk.white(`Title: ${newTask.title}`) +
'\n' +
"\n" +
chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) +
'\n' +
"\n" +
chalk.white(
`Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}`
) +
'\n\n' +
"\n\n" +
dependencyDisplay +
dependencyAnalysis +
'\n' +
chalk.white.bold('Next Steps:') +
'\n' +
"\n" +
chalk.white.bold("Next Steps:") +
"\n" +
chalk.cyan(
`1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details`
) +
'\n' +
"\n" +
chalk.cyan(
`2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it`
) +
'\n' +
"\n" +
chalk.cyan(
`3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks`
),
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
{ padding: 1, borderColor: "green", borderStyle: "round" }
)
);
@@ -1176,19 +1141,19 @@ async function addTask(
if (
aiServiceResponse &&
aiServiceResponse.telemetryData &&
(outputType === 'cli' || outputType === 'text')
(outputType === "cli" || outputType === "text")
) {
displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
displayAiUsageSummary(aiServiceResponse.telemetryData, "cli");
}
}
report(
`DEBUG: Returning new task ID: ${newTaskId} and telemetry.`,
'debug'
"debug"
);
return {
newTaskId: newTaskId,
telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null
telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null,
};
} catch (error) {
// Stop any loading indicator on error
@@ -1196,8 +1161,8 @@ async function addTask(
stopLoadingIndicator(loadingIndicator);
}
report(`Error adding task: ${error.message}`, 'error');
if (outputFormat === 'text') {
report(`Error adding task: ${error.message}`, "error");
if (outputFormat === "text") {
console.error(chalk.red(`Error: ${error.message}`));
}
// In MCP mode, we let the direct function handler catch and format

View File

@@ -1,11 +1,11 @@
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';
import path from "path";
import chalk from "chalk";
import boxen from "boxen";
import Table from "cli-table3";
import { log, readJSON, writeJSON, truncate, isSilentMode } from '../utils.js';
import { displayBanner } from '../ui.js';
import generateTaskFiles from './generate-task-files.js';
import { log, readJSON, writeJSON, truncate, isSilentMode } from "../utils.js";
import { displayBanner } from "../ui.js";
import generateTaskFiles from "./generate-task-files.js";
/**
* Clear subtasks from specified tasks
@@ -13,60 +13,58 @@ import generateTaskFiles from './generate-task-files.js';
* @param {string} taskIds - Task IDs to clear subtasks from
*/
function clearSubtasks(tasksPath, taskIds) {
displayBanner();
log('info', `Reading tasks from ${tasksPath}...`);
log("info", `Reading tasks from ${tasksPath}...`);
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log('error', 'No valid tasks found.');
log("error", "No valid tasks found.");
process.exit(1);
}
if (!isSilentMode()) {
console.log(
boxen(chalk.white.bold('Clearing Subtasks'), {
boxen(chalk.white.bold("Clearing Subtasks"), {
padding: 1,
borderColor: 'blue',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
borderColor: "blue",
borderStyle: "round",
margin: { top: 1, bottom: 1 },
})
);
}
// Handle multiple task IDs (comma-separated)
const taskIdArray = taskIds.split(',').map((id) => id.trim());
const taskIdArray = taskIds.split(",").map((id) => id.trim());
let clearedCount = 0;
// Create a summary table for the cleared subtasks
const summaryTable = new Table({
head: [
chalk.cyan.bold('Task ID'),
chalk.cyan.bold('Task Title'),
chalk.cyan.bold('Subtasks Cleared')
chalk.cyan.bold("Task ID"),
chalk.cyan.bold("Task Title"),
chalk.cyan.bold("Subtasks Cleared"),
],
colWidths: [10, 50, 20],
style: { head: [], border: [] }
style: { head: [], border: [] },
});
taskIdArray.forEach((taskId) => {
const id = parseInt(taskId, 10);
if (isNaN(id)) {
log('error', `Invalid task ID: ${taskId}`);
log("error", `Invalid task ID: ${taskId}`);
return;
}
const task = data.tasks.find((t) => t.id === id);
if (!task) {
log('error', `Task ${id} not found`);
log("error", `Task ${id} not found`);
return;
}
if (!task.subtasks || task.subtasks.length === 0) {
log('info', `Task ${id} has no subtasks to clear`);
log("info", `Task ${id} has no subtasks to clear`);
summaryTable.push([
id.toString(),
truncate(task.title, 47),
chalk.yellow('No subtasks')
chalk.yellow("No subtasks"),
]);
return;
}
@@ -74,12 +72,12 @@ function clearSubtasks(tasksPath, taskIds) {
const subtaskCount = task.subtasks.length;
task.subtasks = [];
clearedCount++;
log('info', `Cleared ${subtaskCount} subtasks from task ${id}`);
log("info", `Cleared ${subtaskCount} subtasks from task ${id}`);
summaryTable.push([
id.toString(),
truncate(task.title, 47),
chalk.green(`${subtaskCount} subtasks cleared`)
chalk.green(`${subtaskCount} subtasks cleared`),
]);
});
@@ -89,18 +87,18 @@ function clearSubtasks(tasksPath, taskIds) {
// Show summary table
if (!isSilentMode()) {
console.log(
boxen(chalk.white.bold('Subtask Clearing Summary:'), {
boxen(chalk.white.bold("Subtask Clearing Summary:"), {
padding: { left: 2, right: 2, top: 0, bottom: 0 },
margin: { top: 1, bottom: 0 },
borderColor: 'blue',
borderStyle: 'round'
borderColor: "blue",
borderStyle: "round",
})
);
console.log(summaryTable.toString());
}
// Regenerate task files to reflect changes
log('info', 'Regenerating task files...');
log("info", "Regenerating task files...");
generateTaskFiles(tasksPath, path.dirname(tasksPath));
// Success message
@@ -112,9 +110,9 @@ function clearSubtasks(tasksPath, taskIds) {
),
{
padding: 1,
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1 }
borderColor: "green",
borderStyle: "round",
margin: { top: 1 },
}
)
);
@@ -122,15 +120,15 @@ function clearSubtasks(tasksPath, taskIds) {
// Next steps suggestion
console.log(
boxen(
chalk.white.bold('Next Steps:') +
'\n\n' +
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master expand --id=<id>')} to generate new subtasks\n` +
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master list --with-subtasks')} to verify changes`,
chalk.white.bold("Next Steps:") +
"\n\n" +
`${chalk.cyan("1.")} Run ${chalk.yellow("task-master expand --id=<id>")} to generate new subtasks\n` +
`${chalk.cyan("2.")} Run ${chalk.yellow("task-master list --with-subtasks")} to verify changes`,
{
padding: 1,
borderColor: 'cyan',
borderStyle: 'round',
margin: { top: 1 }
borderColor: "cyan",
borderStyle: "round",
margin: { top: 1 },
}
)
);
@@ -138,11 +136,11 @@ function clearSubtasks(tasksPath, taskIds) {
} else {
if (!isSilentMode()) {
console.log(
boxen(chalk.yellow('No subtasks were cleared'), {
boxen(chalk.yellow("No subtasks were cleared"), {
padding: 1,
borderColor: 'yellow',
borderStyle: 'round',
margin: { top: 1 }
borderColor: "yellow",
borderStyle: "round",
margin: { top: 1 },
})
);
}

View File

@@ -1,23 +1,23 @@
import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';
import chalk from "chalk";
import boxen from "boxen";
import Table from "cli-table3";
import {
log,
readJSON,
truncate,
readComplexityReport,
addComplexityToTask
} from '../utils.js';
import findNextTask from './find-next-task.js';
addComplexityToTask,
} from "../utils.js";
import findNextTask from "./find-next-task.js";
import {
displayBanner,
getStatusWithColor,
formatDependenciesWithStatus,
getComplexityWithColor,
createProgressBar
} from '../ui.js';
createProgressBar,
} from "../ui.js";
/**
* List all tasks
@@ -33,14 +33,9 @@ function listTasks(
statusFilter,
reportPath = null,
withSubtasks = false,
outputFormat = 'text'
outputFormat = "text"
) {
try {
// Only display banner for text output
if (outputFormat === 'text') {
displayBanner();
}
const data = readJSON(tasksPath); // Reads the whole tasks.json
if (!data || !data.tasks) {
throw new Error(`No valid tasks found in ${tasksPath}`);
@@ -55,7 +50,7 @@ function listTasks(
// Filter tasks by status if specified
const filteredTasks =
statusFilter && statusFilter.toLowerCase() !== 'all' // <-- Added check for 'all'
statusFilter && statusFilter.toLowerCase() !== "all" // <-- Added check for 'all'
? data.tasks.filter(
(task) =>
task.status &&
@@ -66,7 +61,7 @@ function listTasks(
// Calculate completion statistics
const totalTasks = data.tasks.length;
const completedTasks = data.tasks.filter(
(task) => task.status === 'done' || task.status === 'completed'
(task) => task.status === "done" || task.status === "completed"
).length;
const completionPercentage =
totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;
@@ -74,19 +69,19 @@ function listTasks(
// Count statuses for tasks
const doneCount = completedTasks;
const inProgressCount = data.tasks.filter(
(task) => task.status === 'in-progress'
(task) => task.status === "in-progress"
).length;
const pendingCount = data.tasks.filter(
(task) => task.status === 'pending'
(task) => task.status === "pending"
).length;
const blockedCount = data.tasks.filter(
(task) => task.status === 'blocked'
(task) => task.status === "blocked"
).length;
const deferredCount = data.tasks.filter(
(task) => task.status === 'deferred'
(task) => task.status === "deferred"
).length;
const cancelledCount = data.tasks.filter(
(task) => task.status === 'cancelled'
(task) => task.status === "cancelled"
).length;
// Count subtasks and their statuses
@@ -102,22 +97,22 @@ function listTasks(
if (task.subtasks && task.subtasks.length > 0) {
totalSubtasks += task.subtasks.length;
completedSubtasks += task.subtasks.filter(
(st) => st.status === 'done' || st.status === 'completed'
(st) => st.status === "done" || st.status === "completed"
).length;
inProgressSubtasks += task.subtasks.filter(
(st) => st.status === 'in-progress'
(st) => st.status === "in-progress"
).length;
pendingSubtasks += task.subtasks.filter(
(st) => st.status === 'pending'
(st) => st.status === "pending"
).length;
blockedSubtasks += task.subtasks.filter(
(st) => st.status === 'blocked'
(st) => st.status === "blocked"
).length;
deferredSubtasks += task.subtasks.filter(
(st) => st.status === 'deferred'
(st) => st.status === "deferred"
).length;
cancelledSubtasks += task.subtasks.filter(
(st) => st.status === 'cancelled'
(st) => st.status === "cancelled"
).length;
}
});
@@ -126,7 +121,7 @@ function listTasks(
totalSubtasks > 0 ? (completedSubtasks / totalSubtasks) * 100 : 0;
// For JSON output, return structured data
if (outputFormat === 'json') {
if (outputFormat === "json") {
// *** Modification: Remove 'details' field for JSON output ***
const tasksWithoutDetails = filteredTasks.map((task) => {
// <-- USES filteredTasks!
@@ -146,7 +141,7 @@ function listTasks(
return {
tasks: tasksWithoutDetails, // <--- THIS IS THE ARRAY BEING RETURNED
filter: statusFilter || 'all', // Return the actual filter used
filter: statusFilter || "all", // Return the actual filter used
stats: {
total: totalTasks,
completed: doneCount,
@@ -164,9 +159,9 @@ function listTasks(
blocked: blockedSubtasks,
deferred: deferredSubtasks,
cancelled: cancelledSubtasks,
completionPercentage: subtaskCompletionPercentage
}
}
completionPercentage: subtaskCompletionPercentage,
},
},
};
}
@@ -174,22 +169,22 @@ function listTasks(
// Calculate status breakdowns as percentages of total
const taskStatusBreakdown = {
'in-progress': totalTasks > 0 ? (inProgressCount / totalTasks) * 100 : 0,
"in-progress": totalTasks > 0 ? (inProgressCount / totalTasks) * 100 : 0,
pending: totalTasks > 0 ? (pendingCount / totalTasks) * 100 : 0,
blocked: totalTasks > 0 ? (blockedCount / totalTasks) * 100 : 0,
deferred: totalTasks > 0 ? (deferredCount / totalTasks) * 100 : 0,
cancelled: totalTasks > 0 ? (cancelledCount / totalTasks) * 100 : 0
cancelled: totalTasks > 0 ? (cancelledCount / totalTasks) * 100 : 0,
};
const subtaskStatusBreakdown = {
'in-progress':
"in-progress":
totalSubtasks > 0 ? (inProgressSubtasks / totalSubtasks) * 100 : 0,
pending: totalSubtasks > 0 ? (pendingSubtasks / totalSubtasks) * 100 : 0,
blocked: totalSubtasks > 0 ? (blockedSubtasks / totalSubtasks) * 100 : 0,
deferred:
totalSubtasks > 0 ? (deferredSubtasks / totalSubtasks) * 100 : 0,
cancelled:
totalSubtasks > 0 ? (cancelledSubtasks / totalSubtasks) * 100 : 0
totalSubtasks > 0 ? (cancelledSubtasks / totalSubtasks) * 100 : 0,
};
// Create progress bars with status breakdowns
@@ -207,21 +202,21 @@ function listTasks(
// Calculate dependency statistics
const completedTaskIds = new Set(
data.tasks
.filter((t) => t.status === 'done' || t.status === 'completed')
.filter((t) => t.status === "done" || t.status === "completed")
.map((t) => t.id)
);
const tasksWithNoDeps = data.tasks.filter(
(t) =>
t.status !== 'done' &&
t.status !== 'completed' &&
t.status !== "done" &&
t.status !== "completed" &&
(!t.dependencies || t.dependencies.length === 0)
).length;
const tasksWithAllDepsSatisfied = data.tasks.filter(
(t) =>
t.status !== 'done' &&
t.status !== 'completed' &&
t.status !== "done" &&
t.status !== "completed" &&
t.dependencies &&
t.dependencies.length > 0 &&
t.dependencies.every((depId) => completedTaskIds.has(depId))
@@ -229,8 +224,8 @@ function listTasks(
const tasksWithUnsatisfiedDeps = data.tasks.filter(
(t) =>
t.status !== 'done' &&
t.status !== 'completed' &&
t.status !== "done" &&
t.status !== "completed" &&
t.dependencies &&
t.dependencies.length > 0 &&
!t.dependencies.every((depId) => completedTaskIds.has(depId))
@@ -283,7 +278,7 @@ function listTasks(
terminalWidth = process.stdout.columns;
} catch (e) {
// Fallback if columns cannot be determined
log('debug', 'Could not determine terminal width, using default');
log("debug", "Could not determine terminal width, using default");
}
// Ensure we have a reasonable default if detection fails
terminalWidth = terminalWidth || 80;
@@ -293,35 +288,35 @@ function listTasks(
// Create dashboard content
const projectDashboardContent =
chalk.white.bold('Project Dashboard') +
'\n' +
chalk.white.bold("Project Dashboard") +
"\n" +
`Tasks Progress: ${chalk.greenBright(taskProgressBar)} ${completionPercentage.toFixed(0)}%\n` +
`Done: ${chalk.green(doneCount)} In Progress: ${chalk.blue(inProgressCount)} Pending: ${chalk.yellow(pendingCount)} Blocked: ${chalk.red(blockedCount)} Deferred: ${chalk.gray(deferredCount)} Cancelled: ${chalk.gray(cancelledCount)}\n\n` +
`Subtasks Progress: ${chalk.cyan(subtaskProgressBar)} ${subtaskCompletionPercentage.toFixed(0)}%\n` +
`Completed: ${chalk.green(completedSubtasks)}/${totalSubtasks} In Progress: ${chalk.blue(inProgressSubtasks)} Pending: ${chalk.yellow(pendingSubtasks)} Blocked: ${chalk.red(blockedSubtasks)} Deferred: ${chalk.gray(deferredSubtasks)} Cancelled: ${chalk.gray(cancelledSubtasks)}\n\n` +
chalk.cyan.bold('Priority Breakdown:') +
'\n' +
`${chalk.red('•')} ${chalk.white('High priority:')} ${data.tasks.filter((t) => t.priority === 'high').length}\n` +
`${chalk.yellow('•')} ${chalk.white('Medium priority:')} ${data.tasks.filter((t) => t.priority === 'medium').length}\n` +
`${chalk.green('•')} ${chalk.white('Low priority:')} ${data.tasks.filter((t) => t.priority === 'low').length}`;
chalk.cyan.bold("Priority Breakdown:") +
"\n" +
`${chalk.red("•")} ${chalk.white("High priority:")} ${data.tasks.filter((t) => t.priority === "high").length}\n` +
`${chalk.yellow("•")} ${chalk.white("Medium priority:")} ${data.tasks.filter((t) => t.priority === "medium").length}\n` +
`${chalk.green("•")} ${chalk.white("Low priority:")} ${data.tasks.filter((t) => t.priority === "low").length}`;
const dependencyDashboardContent =
chalk.white.bold('Dependency Status & Next Task') +
'\n' +
chalk.cyan.bold('Dependency Metrics:') +
'\n' +
`${chalk.green('•')} ${chalk.white('Tasks with no dependencies:')} ${tasksWithNoDeps}\n` +
`${chalk.green('•')} ${chalk.white('Tasks ready to work on:')} ${tasksReadyToWork}\n` +
`${chalk.yellow('•')} ${chalk.white('Tasks blocked by dependencies:')} ${tasksWithUnsatisfiedDeps}\n` +
`${chalk.magenta('•')} ${chalk.white('Most depended-on task:')} ${mostDependedOnTask ? chalk.cyan(`#${mostDependedOnTaskId} (${maxDependents} dependents)`) : chalk.gray('None')}\n` +
`${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')}
chalk.white.bold("Dependency Status & Next Task") +
"\n" +
chalk.cyan.bold("Dependency Metrics:") +
"\n" +
`${chalk.green("•")} ${chalk.white("Tasks with no dependencies:")} ${tasksWithNoDeps}\n` +
`${chalk.green("•")} ${chalk.white("Tasks ready to work on:")} ${tasksReadyToWork}\n` +
`${chalk.yellow("•")} ${chalk.white("Tasks blocked by dependencies:")} ${tasksWithUnsatisfiedDeps}\n` +
`${chalk.magenta("•")} ${chalk.white("Most depended-on task:")} ${mostDependedOnTask ? chalk.cyan(`#${mostDependedOnTaskId} (${maxDependents} dependents)`) : chalk.gray("None")}\n` +
`${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")}
` +
`Priority: ${nextItem ? chalk.white(nextItem.priority || 'medium') : ''} Dependencies: ${nextItem ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : ''}
`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')}`;
`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
@@ -341,23 +336,23 @@ function listTasks(
// Create boxen options with precise widths
const dashboardBox = boxen(projectDashboardContent, {
padding: 1,
borderColor: 'blue',
borderStyle: 'round',
borderColor: "blue",
borderStyle: "round",
width: boxContentWidth,
dimBorder: false
dimBorder: false,
});
const dependencyBox = boxen(dependencyDashboardContent, {
padding: 1,
borderColor: 'magenta',
borderStyle: 'round',
borderColor: "magenta",
borderStyle: "round",
width: boxContentWidth,
dimBorder: false
dimBorder: false,
});
// Create a better side-by-side layout with exact spacing
const dashboardLines = dashboardBox.split('\n');
const dependencyLines = dependencyBox.split('\n');
const dashboardLines = dashboardBox.split("\n");
const dependencyLines = dependencyBox.split("\n");
// Make sure both boxes have the same height
const maxHeight = Math.max(dashboardLines.length, dependencyLines.length);
@@ -367,35 +362,35 @@ function listTasks(
const combinedLines = [];
for (let i = 0; i < maxHeight; i++) {
// Get the dashboard line (or empty string if we've run out of lines)
const dashLine = i < dashboardLines.length ? dashboardLines[i] : '';
const dashLine = i < dashboardLines.length ? dashboardLines[i] : "";
// Get the dependency line (or empty string if we've run out of lines)
const depLine = i < dependencyLines.length ? dependencyLines[i] : '';
const depLine = i < dependencyLines.length ? dependencyLines[i] : "";
// Remove any trailing spaces from dashLine before padding to exact width
const trimmedDashLine = dashLine.trimEnd();
// Pad the dashboard line to exactly halfWidth chars with no extra spaces
const paddedDashLine = trimmedDashLine.padEnd(halfWidth, ' ');
const paddedDashLine = trimmedDashLine.padEnd(halfWidth, " ");
// Join the lines with no space in between
combinedLines.push(paddedDashLine + depLine);
}
// Join all lines and output
console.log(combinedLines.join('\n'));
console.log(combinedLines.join("\n"));
} else {
// Terminal too narrow, show boxes stacked vertically
const dashboardBox = boxen(projectDashboardContent, {
padding: 1,
borderColor: 'blue',
borderStyle: 'round',
margin: { top: 0, bottom: 1 }
borderColor: "blue",
borderStyle: "round",
margin: { top: 0, bottom: 1 },
});
const dependencyBox = boxen(dependencyDashboardContent, {
padding: 1,
borderColor: 'magenta',
borderStyle: 'round',
margin: { top: 0, bottom: 1 }
borderColor: "magenta",
borderStyle: "round",
margin: { top: 0, bottom: 1 },
});
// Display stacked vertically
@@ -408,8 +403,8 @@ function listTasks(
boxen(
statusFilter
? chalk.yellow(`No tasks with status '${statusFilter}' found`)
: chalk.yellow('No tasks found'),
{ padding: 1, borderColor: 'yellow', borderStyle: 'round' }
: chalk.yellow("No tasks found"),
{ padding: 1, borderColor: "yellow", borderStyle: "round" }
)
);
return;
@@ -458,12 +453,12 @@ function listTasks(
// Create a table with correct borders and spacing
const table = new Table({
head: [
chalk.cyan.bold('ID'),
chalk.cyan.bold('Title'),
chalk.cyan.bold('Status'),
chalk.cyan.bold('Priority'),
chalk.cyan.bold('Dependencies'),
chalk.cyan.bold('Complexity')
chalk.cyan.bold("ID"),
chalk.cyan.bold("Title"),
chalk.cyan.bold("Status"),
chalk.cyan.bold("Priority"),
chalk.cyan.bold("Dependencies"),
chalk.cyan.bold("Complexity"),
],
colWidths: [
idWidth,
@@ -471,21 +466,21 @@ function listTasks(
statusWidth,
priorityWidth,
depsWidth,
complexityWidth // Added complexity column width
complexityWidth, // Added complexity column width
],
style: {
head: [], // No special styling for header
border: [], // No special styling for border
compact: false // Use default spacing
compact: false, // Use default spacing
},
wordWrap: true,
wrapOnWordBoundary: true
wrapOnWordBoundary: true,
});
// Process tasks for the table
filteredTasks.forEach((task) => {
// Format dependencies with status indicators (colored)
let depText = 'None';
let depText = "None";
if (task.dependencies && task.dependencies.length > 0) {
// Use the proper formatDependenciesWithStatus function for colored status
depText = formatDependenciesWithStatus(
@@ -495,19 +490,19 @@ function listTasks(
complexityReport
);
} else {
depText = chalk.gray('None');
depText = chalk.gray("None");
}
// Clean up any ANSI codes or confusing characters
const cleanTitle = task.title.replace(/\n/g, ' ');
const cleanTitle = task.title.replace(/\n/g, " ");
// Get priority color
const priorityColor =
{
high: chalk.red,
medium: chalk.yellow,
low: chalk.gray
}[task.priority || 'medium'] || chalk.white;
low: chalk.gray,
}[task.priority || "medium"] || chalk.white;
// Format status
const status = getStatusWithColor(task.status, true);
@@ -517,38 +512,38 @@ function listTasks(
task.id.toString(),
truncate(cleanTitle, titleWidth - 3),
status,
priorityColor(truncate(task.priority || 'medium', priorityWidth - 2)),
priorityColor(truncate(task.priority || "medium", priorityWidth - 2)),
depText,
task.complexityScore
? getComplexityWithColor(task.complexityScore)
: chalk.gray('N/A')
: chalk.gray("N/A"),
]);
// Add subtasks if requested
if (withSubtasks && task.subtasks && task.subtasks.length > 0) {
task.subtasks.forEach((subtask) => {
// Format subtask dependencies with status indicators
let subtaskDepText = 'None';
let subtaskDepText = "None";
if (subtask.dependencies && subtask.dependencies.length > 0) {
// Handle both subtask-to-subtask and subtask-to-task dependencies
const formattedDeps = subtask.dependencies
.map((depId) => {
// Check if it's a dependency on another subtask
if (typeof depId === 'number' && depId < 100) {
if (typeof depId === "number" && depId < 100) {
const foundSubtask = task.subtasks.find(
(st) => st.id === depId
);
if (foundSubtask) {
const isDone =
foundSubtask.status === 'done' ||
foundSubtask.status === 'completed';
const isInProgress = foundSubtask.status === 'in-progress';
foundSubtask.status === "done" ||
foundSubtask.status === "completed";
const isInProgress = foundSubtask.status === "in-progress";
// Use consistent color formatting instead of emojis
if (isDone) {
return chalk.green.bold(`${task.id}.${depId}`);
} else if (isInProgress) {
return chalk.hex('#FFA500').bold(`${task.id}.${depId}`);
return chalk.hex("#FFA500").bold(`${task.id}.${depId}`);
} else {
return chalk.red.bold(`${task.id}.${depId}`);
}
@@ -560,22 +555,22 @@ function listTasks(
// Add complexity to depTask before checking status
addComplexityToTask(depTask, complexityReport);
const isDone =
depTask.status === 'done' || depTask.status === 'completed';
const isInProgress = depTask.status === 'in-progress';
depTask.status === "done" || depTask.status === "completed";
const isInProgress = depTask.status === "in-progress";
// Use the same color scheme as in formatDependenciesWithStatus
if (isDone) {
return chalk.green.bold(`${depId}`);
} else if (isInProgress) {
return chalk.hex('#FFA500').bold(`${depId}`);
return chalk.hex("#FFA500").bold(`${depId}`);
} else {
return chalk.red.bold(`${depId}`);
}
}
return chalk.cyan(depId.toString());
})
.join(', ');
.join(", ");
subtaskDepText = formattedDeps || chalk.gray('None');
subtaskDepText = formattedDeps || chalk.gray("None");
}
// Add the subtask row without truncating dependencies
@@ -583,11 +578,11 @@ function listTasks(
`${task.id}.${subtask.id}`,
chalk.dim(`└─ ${truncate(subtask.title, titleWidth - 5)}`),
getStatusWithColor(subtask.status, true),
chalk.dim('-'),
chalk.dim("-"),
subtaskDepText,
subtask.complexityScore
? chalk.gray(`${subtask.complexityScore}`)
: chalk.gray('N/A')
: chalk.gray("N/A"),
]);
});
}
@@ -597,12 +592,12 @@ function listTasks(
try {
console.log(table.toString());
} catch (err) {
log('error', `Error rendering table: ${err.message}`);
log("error", `Error rendering table: ${err.message}`);
// Fall back to simpler output
console.log(
chalk.yellow(
'\nFalling back to simple task list due to terminal width constraints:'
"\nFalling back to simple task list due to terminal width constraints:"
)
);
filteredTasks.forEach((task) => {
@@ -624,13 +619,13 @@ function listTasks(
const priorityColors = {
high: chalk.red.bold,
medium: chalk.yellow,
low: chalk.gray
low: chalk.gray,
};
// Show next task box in a prominent color
if (nextItem) {
// Prepare subtasks section if they exist (Only tasks have .subtasks property)
let subtasksSection = '';
let subtasksSection = "";
// Check if the nextItem is a top-level task before looking for subtasks
const parentTaskForSubtasks = data.tasks.find(
(t) => String(t.id) === String(nextItem.id)
@@ -640,75 +635,75 @@ function listTasks(
parentTaskForSubtasks.subtasks &&
parentTaskForSubtasks.subtasks.length > 0
) {
subtasksSection = `\n\n${chalk.white.bold('Subtasks:')}\n`;
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 status = subtask.status || "pending";
const statusColors = {
done: chalk.green,
completed: chalk.green,
pending: chalk.yellow,
'in-progress': chalk.blue,
"in-progress": chalk.blue,
deferred: chalk.gray,
blocked: chalk.red,
cancelled: chalk.gray
cancelled: chalk.gray,
};
const statusColor =
statusColors[status.toLowerCase()] || chalk.white;
// Ensure subtask ID is displayed correctly using parent ID from the original task object
return `${chalk.cyan(`${parentTaskForSubtasks.id}.${subtask.id}`)} [${statusColor(status)}] ${subtask.title}`;
})
.join('\n');
.join("\n");
}
console.log(
boxen(
chalk.hex('#FF8800').bold(
chalk.hex("#FF8800").bold(
// Use nextItem.id and nextItem.title
`🔥 Next Task to Work On: #${nextItem.id} - ${nextItem.title}`
) +
'\n\n' +
"\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, complexityReport) : chalk.gray('None')}\n\n` +
`${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, 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)}` +
`${chalk.white("Description:")} ${getWorkItemDescription(nextItem, data.tasks)}` +
subtasksSection + // <-- Subtasks are handled above now
'\n\n' +
"\n\n" +
// Use nextItem.id
`${chalk.cyan('Start working:')} ${chalk.yellow(`task-master set-status --id=${nextItem.id} --status=in-progress`)}\n` +
`${chalk.cyan("Start working:")} ${chalk.yellow(`task-master set-status --id=${nextItem.id} --status=in-progress`)}\n` +
// Use nextItem.id
`${chalk.cyan('View details:')} ${chalk.yellow(`task-master show ${nextItem.id}`)}`,
`${chalk.cyan("View details:")} ${chalk.yellow(`task-master show ${nextItem.id}`)}`,
{
padding: { left: 2, right: 2, top: 1, bottom: 1 },
borderColor: '#FF8800',
borderStyle: 'round',
borderColor: "#FF8800",
borderStyle: "round",
margin: { top: 1, bottom: 1 },
title: '⚡ RECOMMENDED NEXT TASK ⚡',
titleAlignment: 'center',
title: "⚡ RECOMMENDED NEXT TASK ⚡",
titleAlignment: "center",
width: terminalWidth - 4,
fullscreen: false
fullscreen: false,
}
)
);
} else {
console.log(
boxen(
chalk.hex('#FF8800').bold('No eligible next task found') +
'\n\n' +
'All pending tasks have dependencies that are not yet completed, or all tasks are done.',
chalk.hex("#FF8800").bold("No eligible next task found") +
"\n\n" +
"All pending tasks have dependencies that are not yet completed, or all tasks are done.",
{
padding: 1,
borderColor: '#FF8800',
borderStyle: 'round',
borderColor: "#FF8800",
borderStyle: "round",
margin: { top: 1, bottom: 1 },
title: '⚡ NEXT TASK ⚡',
titleAlignment: 'center',
width: terminalWidth - 4 // Use full terminal width minus a small margin
title: "⚡ NEXT TASK ⚡",
titleAlignment: "center",
width: terminalWidth - 4, // Use full terminal width minus a small margin
}
)
);
@@ -717,28 +712,28 @@ function listTasks(
// Show next steps
console.log(
boxen(
chalk.white.bold('Suggested Next Steps:') +
'\n\n' +
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master next')} to see what to work on next\n` +
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks\n` +
`${chalk.cyan('3.')} Run ${chalk.yellow('task-master set-status --id=<id> --status=done')} to mark a task as complete`,
chalk.white.bold("Suggested Next Steps:") +
"\n\n" +
`${chalk.cyan("1.")} Run ${chalk.yellow("task-master next")} to see what to work on next\n` +
`${chalk.cyan("2.")} Run ${chalk.yellow("task-master expand --id=<id>")} to break down a task into subtasks\n` +
`${chalk.cyan("3.")} Run ${chalk.yellow("task-master set-status --id=<id> --status=done")} to mark a task as complete`,
{
padding: 1,
borderColor: 'gray',
borderStyle: 'round',
margin: { top: 1 }
borderColor: "gray",
borderStyle: "round",
margin: { top: 1 },
}
)
);
} catch (error) {
log('error', `Error listing tasks: ${error.message}`);
log("error", `Error listing tasks: ${error.message}`);
if (outputFormat === 'json') {
if (outputFormat === "json") {
// Return structured error for JSON output
throw {
code: 'TASK_LIST_ERROR',
code: "TASK_LIST_ERROR",
message: error.message,
details: error.stack
details: error.stack,
};
}
@@ -749,18 +744,18 @@ function listTasks(
// *** Helper function to get description for task or subtask ***
function getWorkItemDescription(item, allTasks) {
if (!item) return 'N/A';
if (!item) return "N/A";
if (item.parentId) {
// It's a subtask
const parent = allTasks.find((t) => t.id === item.parentId);
const subtask = parent?.subtasks?.find(
(st) => `${parent.id}.${st.id}` === item.id
);
return subtask?.description || 'No description available.';
return subtask?.description || "No description available.";
} else {
// It's a top-level task
const task = allTasks.find((t) => String(t.id) === String(item.id));
return task?.description || 'No description available.';
return task?.description || "No description available.";
}
}

View File

@@ -1,17 +1,17 @@
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import path from "path";
import chalk from "chalk";
import boxen from "boxen";
import { log, readJSON, writeJSON, findTaskById } from '../utils.js';
import { displayBanner } from '../ui.js';
import { validateTaskDependencies } from '../dependency-manager.js';
import { getDebugFlag } from '../config-manager.js';
import updateSingleTaskStatus from './update-single-task-status.js';
import generateTaskFiles from './generate-task-files.js';
import { log, readJSON, writeJSON, findTaskById } from "../utils.js";
import { displayBanner } from "../ui.js";
import { validateTaskDependencies } from "../dependency-manager.js";
import { getDebugFlag } from "../config-manager.js";
import updateSingleTaskStatus from "./update-single-task-status.js";
import generateTaskFiles from "./generate-task-files.js";
import {
isValidTaskStatus,
TASK_STATUS_OPTIONS
} from '../../../src/constants/task-status.js';
TASK_STATUS_OPTIONS,
} from "../../../src/constants/task-status.js";
/**
* Set the status of a task
@@ -25,7 +25,7 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
try {
if (!isValidTaskStatus(newStatus)) {
throw new Error(
`Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(', ')}`
`Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(", ")}`
);
}
// Determine if we're in MCP mode by checking for mcpLog
@@ -33,25 +33,23 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
// Only display UI elements if not in MCP mode
if (!isMcpMode) {
displayBanner();
console.log(
boxen(chalk.white.bold(`Updating Task Status to: ${newStatus}`), {
padding: 1,
borderColor: 'blue',
borderStyle: 'round'
borderColor: "blue",
borderStyle: "round",
})
);
}
log('info', `Reading tasks from ${tasksPath}...`);
log("info", `Reading tasks from ${tasksPath}...`);
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
throw new Error(`No valid tasks found in ${tasksPath}`);
}
// Handle multiple task IDs (comma-separated)
const taskIds = taskIdInput.split(',').map((id) => id.trim());
const taskIds = taskIdInput.split(",").map((id) => id.trim());
const updatedTasks = [];
// Update each task
@@ -64,13 +62,13 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
writeJSON(tasksPath, data);
// Validate dependencies after status update
log('info', 'Validating dependencies after status update...');
log("info", "Validating dependencies after status update...");
validateTaskDependencies(data.tasks);
// Generate individual task files
log('info', 'Regenerating task files...');
log("info", "Regenerating task files...");
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
mcpLog: options.mcpLog
mcpLog: options.mcpLog,
});
// Display success message - only in CLI mode
@@ -82,10 +80,10 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
console.log(
boxen(
chalk.white.bold(`Successfully updated task ${id} status:`) +
'\n' +
`From: ${chalk.yellow(task ? task.status : 'unknown')}\n` +
"\n" +
`From: ${chalk.yellow(task ? task.status : "unknown")}\n` +
`To: ${chalk.green(newStatus)}`,
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
{ padding: 1, borderColor: "green", borderStyle: "round" }
)
);
}
@@ -96,11 +94,11 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
success: true,
updatedTasks: updatedTasks.map((id) => ({
id,
status: newStatus
}))
status: newStatus,
})),
};
} catch (error) {
log('error', `Error setting task status: ${error.message}`);
log("error", `Error setting task status: ${error.message}`);
// Only show error UI in CLI mode
if (!options?.mcpLog) {

View File

@@ -40,7 +40,7 @@ const warmGradient = gradient(['#fb8b24', '#e36414', '#9a031e']);
function displayBanner() {
if (isSilentMode()) return;
console.clear();
// console.clear(); // Removing this to avoid clearing the terminal per command
const bannerText = figlet.textSync('Task Master', {
font: 'Standard',
horizontalLayout: 'default',
@@ -78,6 +78,8 @@ function displayBanner() {
* @returns {Object} Spinner object
*/
function startLoadingIndicator(message) {
if (isSilentMode()) return null;
const spinner = ora({
text: message,
color: 'cyan'
@@ -87,15 +89,75 @@ function startLoadingIndicator(message) {
}
/**
* Stop a loading indicator
* Stop a loading indicator (basic stop, no success/fail indicator)
* @param {Object} spinner - Spinner object to stop
*/
function stopLoadingIndicator(spinner) {
if (spinner && spinner.stop) {
if (spinner && typeof spinner.stop === 'function') {
spinner.stop();
}
}
/**
* Complete a loading indicator with success (shows checkmark)
* @param {Object} spinner - Spinner object to complete
* @param {string} message - Optional success message (defaults to current text)
*/
function succeedLoadingIndicator(spinner, message = null) {
if (spinner && typeof spinner.succeed === 'function') {
if (message) {
spinner.succeed(message);
} else {
spinner.succeed();
}
}
}
/**
* Complete a loading indicator with failure (shows X)
* @param {Object} spinner - Spinner object to fail
* @param {string} message - Optional failure message (defaults to current text)
*/
function failLoadingIndicator(spinner, message = null) {
if (spinner && typeof spinner.fail === 'function') {
if (message) {
spinner.fail(message);
} else {
spinner.fail();
}
}
}
/**
* Complete a loading indicator with warning (shows warning symbol)
* @param {Object} spinner - Spinner object to warn
* @param {string} message - Optional warning message (defaults to current text)
*/
function warnLoadingIndicator(spinner, message = null) {
if (spinner && typeof spinner.warn === 'function') {
if (message) {
spinner.warn(message);
} else {
spinner.warn();
}
}
}
/**
* Complete a loading indicator with info (shows info symbol)
* @param {Object} spinner - Spinner object to complete with info
* @param {string} message - Optional info message (defaults to current text)
*/
function infoLoadingIndicator(spinner, message = null) {
if (spinner && typeof spinner.info === 'function') {
if (message) {
spinner.info(message);
} else {
spinner.info();
}
}
}
/**
* Create a colored progress bar
* @param {number} percent - The completion percentage
@@ -232,14 +294,14 @@ function getStatusWithColor(status, forTable = false) {
}
const statusConfig = {
done: { color: chalk.green, icon: '', tableIcon: '✓' },
completed: { color: chalk.green, icon: '', tableIcon: '✓' },
pending: { color: chalk.yellow, icon: '⏱️', tableIcon: '⏱' },
done: { color: chalk.green, icon: '', tableIcon: '✓' },
completed: { color: chalk.green, icon: '', tableIcon: '✓' },
pending: { color: chalk.yellow, icon: '', tableIcon: '⏱' },
'in-progress': { color: chalk.hex('#FFA500'), icon: '🔄', tableIcon: '►' },
deferred: { color: chalk.gray, icon: '⏱️', tableIcon: '⏱' },
blocked: { color: chalk.red, icon: '', tableIcon: '✗' },
review: { color: chalk.magenta, icon: '👀', tableIcon: '👁' },
cancelled: { color: chalk.gray, icon: '❌', tableIcon: '' }
deferred: { color: chalk.gray, icon: 'x', tableIcon: '⏱' },
blocked: { color: chalk.red, icon: '!', tableIcon: '✗' },
review: { color: chalk.magenta, icon: '?', tableIcon: '?' },
cancelled: { color: chalk.gray, icon: '❌', tableIcon: 'x' }
};
const config = statusConfig[status.toLowerCase()] || {
@@ -383,8 +445,6 @@ function formatDependenciesWithStatus(
* Display a comprehensive help guide
*/
function displayHelp() {
displayBanner();
// Get terminal width - moved to top of function to make it available throughout
const terminalWidth = process.stdout.columns || 100; // Default to 100 if can't detect
@@ -772,8 +832,6 @@ function truncateString(str, maxLength) {
* @param {string} tasksPath - Path to the tasks.json file
*/
async function displayNextTask(tasksPath, complexityReportPath = null) {
displayBanner();
// Read the tasks file
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
@@ -1044,8 +1102,6 @@ async function displayTaskById(
complexityReportPath = null,
statusFilter = null
) {
displayBanner();
// Read the tasks file
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
@@ -1500,8 +1556,6 @@ async function displayTaskById(
* @param {string} reportPath - Path to the complexity report file
*/
async function displayComplexityReport(reportPath) {
displayBanner();
// Check if the report exists
if (!fs.existsSync(reportPath)) {
console.log(
@@ -2472,5 +2526,8 @@ export {
displayModelConfiguration,
displayAvailableModels,
displayAiUsageSummary,
displayMultipleTasksSummary
succeedLoadingIndicator,
failLoadingIndicator,
warnLoadingIndicator,
infoLoadingIndicator
};