diff --git a/.taskmaster/config.json b/.taskmaster/config.json index 20cdb49c..442dfc1c 100644 --- a/.taskmaster/config.json +++ b/.taskmaster/config.json @@ -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 }, diff --git a/.taskmaster/tasks/task_097.txt b/.taskmaster/tasks/task_097.txt deleted file mode 100644 index 2ef43dd6..00000000 --- a/.taskmaster/tasks/task_097.txt +++ /dev/null @@ -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 diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index 8cc174d5..c8cd9c33 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -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": [] } ] } \ No newline at end of file diff --git a/scripts/modules/dependency-manager.js b/scripts/modules/dependency-manager.js index 73745276..a6417780 100644 --- a/scripts/modules/dependency-manager.js +++ b/scripts/modules/dependency-manager.js @@ -3,23 +3,23 @@ * 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, - readJSON, - writeJSON, - taskExists, - formatTaskId, - findCycles, - isSilentMode -} from './utils.js'; + log, + readJSON, + writeJSON, + taskExists, + formatTaskId, + findCycles, + 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,183 +28,183 @@ 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'); - process.exit(1); - } + const data = readJSON(tasksPath); + if (!data || !data.tasks) { + 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('.') - ? taskId - : parseInt(taskId, 10); + // Format the task and dependency IDs correctly + const formattedTaskId = + typeof taskId === "string" && taskId.includes(".") + ? taskId + : parseInt(taskId, 10); - const formattedDependencyId = formatTaskId(dependencyId); + const formattedDependencyId = formatTaskId(dependencyId); - // Check if the dependency task or subtask actually exists - if (!taskExists(data.tasks, formattedDependencyId)) { - log( - 'error', - `Dependency target ${formattedDependencyId} does not exist in tasks.json` - ); - process.exit(1); - } + // Check if the dependency task or subtask actually exists + if (!taskExists(data.tasks, formattedDependencyId)) { + log( + "error", + `Dependency target ${formattedDependencyId} does not exist in tasks.json` + ); + process.exit(1); + } - // Find the task to update - let targetTask = null; - let isSubtask = false; + // Find the task to update + let targetTask = null; + let isSubtask = false; - if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) { - // Handle dot notation for subtasks (e.g., "1.2") - const [parentId, subtaskId] = formattedTaskId - .split('.') - .map((id) => parseInt(id, 10)); - const parentTask = data.tasks.find((t) => t.id === parentId); + if (typeof formattedTaskId === "string" && formattedTaskId.includes(".")) { + // Handle dot notation for subtasks (e.g., "1.2") + const [parentId, subtaskId] = formattedTaskId + .split(".") + .map((id) => parseInt(id, 10)); + const parentTask = data.tasks.find((t) => t.id === parentId); - if (!parentTask) { - log('error', `Parent task ${parentId} not found.`); - process.exit(1); - } + if (!parentTask) { + log("error", `Parent task ${parentId} not found.`); + process.exit(1); + } - if (!parentTask.subtasks) { - log('error', `Parent task ${parentId} has no subtasks.`); - process.exit(1); - } + if (!parentTask.subtasks) { + log("error", `Parent task ${parentId} has no subtasks.`); + process.exit(1); + } - targetTask = parentTask.subtasks.find((s) => s.id === subtaskId); - isSubtask = true; + targetTask = parentTask.subtasks.find((s) => s.id === subtaskId); + isSubtask = true; - if (!targetTask) { - log('error', `Subtask ${formattedTaskId} not found.`); - process.exit(1); - } - } else { - // Regular task (not a subtask) - targetTask = data.tasks.find((t) => t.id === formattedTaskId); + if (!targetTask) { + log("error", `Subtask ${formattedTaskId} not found.`); + process.exit(1); + } + } else { + // Regular task (not a subtask) + targetTask = data.tasks.find((t) => t.id === formattedTaskId); - if (!targetTask) { - log('error', `Task ${formattedTaskId} not found.`); - process.exit(1); - } - } + if (!targetTask) { + log("error", `Task ${formattedTaskId} not found.`); + process.exit(1); + } + } - // Initialize dependencies array if it doesn't exist - if (!targetTask.dependencies) { - targetTask.dependencies = []; - } + // Initialize dependencies array if it doesn't exist + if (!targetTask.dependencies) { + targetTask.dependencies = []; + } - // Check if dependency already exists - if ( - targetTask.dependencies.some((d) => { - // Convert both to strings for comparison to handle both numeric and string IDs - return String(d) === String(formattedDependencyId); - }) - ) { - log( - 'warn', - `Dependency ${formattedDependencyId} already exists in task ${formattedTaskId}.` - ); - return; - } + // Check if dependency already exists + if ( + targetTask.dependencies.some((d) => { + // Convert both to strings for comparison to handle both numeric and string IDs + return String(d) === String(formattedDependencyId); + }) + ) { + log( + "warn", + `Dependency ${formattedDependencyId} already exists in task ${formattedTaskId}.` + ); + return; + } - // 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.`); - process.exit(1); - } + // 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.`); + process.exit(1); + } - // For subtasks of the same parent, we need to make sure we're not treating it as a self-dependency - // Check if we're dealing with subtasks with the same parent task - let isSelfDependency = false; + // For subtasks of the same parent, we need to make sure we're not treating it as a self-dependency + // Check if we're dealing with subtasks with the same parent task + let isSelfDependency = false; - if ( - typeof formattedTaskId === 'string' && - typeof formattedDependencyId === 'string' && - formattedTaskId.includes('.') && - formattedDependencyId.includes('.') - ) { - const [taskParentId] = formattedTaskId.split('.'); - const [depParentId] = formattedDependencyId.split('.'); + if ( + typeof formattedTaskId === "string" && + typeof formattedDependencyId === "string" && + formattedTaskId.includes(".") && + formattedDependencyId.includes(".") + ) { + 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; + // 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', - `Adding dependency between subtasks: ${formattedTaskId} depends on ${formattedDependencyId}` - ); - log( - 'debug', - `Parent IDs: ${taskParentId} and ${depParentId}, Self-dependency check: ${isSelfDependency}` - ); - } + // Log for debugging + log( + "debug", + `Adding dependency between subtasks: ${formattedTaskId} depends on ${formattedDependencyId}` + ); + log( + "debug", + `Parent IDs: ${taskParentId} and ${depParentId}, Self-dependency check: ${isSelfDependency}` + ); + } - if (isSelfDependency) { - log('error', `Subtask ${formattedTaskId} cannot depend on itself.`); - process.exit(1); - } + if (isSelfDependency) { + log("error", `Subtask ${formattedTaskId} cannot depend on itself.`); + process.exit(1); + } - // Check for circular dependencies - let dependencyChain = [formattedTaskId]; - if ( - !isCircularDependency(data.tasks, formattedDependencyId, dependencyChain) - ) { - // Add the dependency - targetTask.dependencies.push(formattedDependencyId); + // Check for circular dependencies + let dependencyChain = [formattedTaskId]; + if ( + !isCircularDependency(data.tasks, formattedDependencyId, dependencyChain) + ) { + // Add the dependency + targetTask.dependencies.push(formattedDependencyId); - // Sort dependencies numerically or by parent task ID first, then subtask ID - targetTask.dependencies.sort((a, b) => { - 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); - return aParent !== bParent ? aParent - bParent : aChild - bChild; - } else if (typeof a === 'number') { - return -1; // Numbers come before strings - } else { - return 1; // Strings come after numbers - } - }); + // Sort dependencies numerically or by parent task ID first, then subtask ID + targetTask.dependencies.sort((a, b) => { + 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); + return aParent !== bParent ? aParent - bParent : aChild - bChild; + } else if (typeof a === "number") { + return -1; // Numbers come before strings + } else { + return 1; // Strings come after numbers + } + }); - // Save changes - writeJSON(tasksPath, data); - log( - 'success', - `Added dependency ${formattedDependencyId} to task ${formattedTaskId}` - ); + // Save changes + writeJSON(tasksPath, data); + log( + "success", + `Added dependency ${formattedDependencyId} to task ${formattedTaskId}` + ); - // Display a more visually appealing success message - if (!isSilentMode()) { - console.log( - boxen( - chalk.green(`Successfully added dependency:\n\n`) + - `Task ${chalk.bold(formattedTaskId)} now depends on ${chalk.bold(formattedDependencyId)}`, - { - padding: 1, - borderColor: 'green', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); - } + // Display a more visually appealing success message + if (!isSilentMode()) { + console.log( + boxen( + chalk.green(`Successfully added dependency:\n\n`) + + `Task ${chalk.bold(formattedTaskId)} now depends on ${chalk.bold(formattedDependencyId)}`, + { + padding: 1, + borderColor: "green", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); + } - // Generate updated task files - await generateTaskFiles(tasksPath, path.dirname(tasksPath)); + // Generate updated task files + await generateTaskFiles(tasksPath, path.dirname(tasksPath)); - log('info', 'Task files regenerated with updated dependencies.'); - } else { - log( - 'error', - `Cannot add dependency ${formattedDependencyId} to task ${formattedTaskId} as it would create a circular dependency.` - ); - process.exit(1); - } + log("info", "Task files regenerated with updated dependencies."); + } else { + log( + "error", + `Cannot add dependency ${formattedDependencyId} to task ${formattedTaskId} as it would create a circular dependency.` + ); + process.exit(1); + } } /** @@ -214,127 +214,127 @@ 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.'); - process.exit(1); - } + // Read tasks file + const data = readJSON(tasksPath); + if (!data || !data.tasks) { + log("error", "No valid tasks found."); + process.exit(1); + } - // Format the task and dependency IDs correctly - const formattedTaskId = - typeof taskId === 'string' && taskId.includes('.') - ? taskId - : parseInt(taskId, 10); + // Format the task and dependency IDs correctly + const formattedTaskId = + typeof taskId === "string" && taskId.includes(".") + ? taskId + : parseInt(taskId, 10); - const formattedDependencyId = formatTaskId(dependencyId); + const formattedDependencyId = formatTaskId(dependencyId); - // Find the task to update - let targetTask = null; - let isSubtask = false; + // Find the task to update + let targetTask = null; + let isSubtask = false; - if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) { - // Handle dot notation for subtasks (e.g., "1.2") - const [parentId, subtaskId] = formattedTaskId - .split('.') - .map((id) => parseInt(id, 10)); - const parentTask = data.tasks.find((t) => t.id === parentId); + if (typeof formattedTaskId === "string" && formattedTaskId.includes(".")) { + // Handle dot notation for subtasks (e.g., "1.2") + const [parentId, subtaskId] = formattedTaskId + .split(".") + .map((id) => parseInt(id, 10)); + const parentTask = data.tasks.find((t) => t.id === parentId); - if (!parentTask) { - log('error', `Parent task ${parentId} not found.`); - process.exit(1); - } + if (!parentTask) { + log("error", `Parent task ${parentId} not found.`); + process.exit(1); + } - if (!parentTask.subtasks) { - log('error', `Parent task ${parentId} has no subtasks.`); - process.exit(1); - } + if (!parentTask.subtasks) { + log("error", `Parent task ${parentId} has no subtasks.`); + process.exit(1); + } - targetTask = parentTask.subtasks.find((s) => s.id === subtaskId); - isSubtask = true; + targetTask = parentTask.subtasks.find((s) => s.id === subtaskId); + isSubtask = true; - if (!targetTask) { - log('error', `Subtask ${formattedTaskId} not found.`); - process.exit(1); - } - } else { - // Regular task (not a subtask) - targetTask = data.tasks.find((t) => t.id === formattedTaskId); + if (!targetTask) { + log("error", `Subtask ${formattedTaskId} not found.`); + process.exit(1); + } + } else { + // Regular task (not a subtask) + targetTask = data.tasks.find((t) => t.id === formattedTaskId); - if (!targetTask) { - log('error', `Task ${formattedTaskId} not found.`); - process.exit(1); - } - } + if (!targetTask) { + log("error", `Task ${formattedTaskId} not found.`); + process.exit(1); + } + } - // Check if the task has any dependencies - if (!targetTask.dependencies || targetTask.dependencies.length === 0) { - log( - 'info', - `Task ${formattedTaskId} has no dependencies, nothing to remove.` - ); - return; - } + // Check if the task has any dependencies + if (!targetTask.dependencies || targetTask.dependencies.length === 0) { + log( + "info", + `Task ${formattedTaskId} has no dependencies, nothing to remove.` + ); + return; + } - // Normalize the dependency ID for comparison to handle different formats - const normalizedDependencyId = String(formattedDependencyId); + // Normalize the dependency ID for comparison to handle different formats + const normalizedDependencyId = String(formattedDependencyId); - // Check if the dependency exists by comparing string representations - const dependencyIndex = targetTask.dependencies.findIndex((dep) => { - // Convert both to strings for comparison - let depStr = String(dep); + // Check if the dependency exists by comparing string representations + const dependencyIndex = targetTask.dependencies.findIndex((dep) => { + // Convert both to strings for comparison + let depStr = String(dep); - // Special handling for numeric IDs that might be subtask references - 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('.'); - depStr = `${parentId}.${dep}`; - } + // Special handling for numeric IDs that might be subtask references + 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("."); + depStr = `${parentId}.${dep}`; + } - return depStr === normalizedDependencyId; - }); + return depStr === normalizedDependencyId; + }); - if (dependencyIndex === -1) { - log( - 'info', - `Task ${formattedTaskId} does not depend on ${formattedDependencyId}, no changes made.` - ); - return; - } + if (dependencyIndex === -1) { + log( + "info", + `Task ${formattedTaskId} does not depend on ${formattedDependencyId}, no changes made.` + ); + return; + } - // Remove the dependency - targetTask.dependencies.splice(dependencyIndex, 1); + // Remove the dependency + targetTask.dependencies.splice(dependencyIndex, 1); - // Save the updated tasks - writeJSON(tasksPath, data); + // Save the updated tasks + writeJSON(tasksPath, data); - // Success message - log( - 'success', - `Removed dependency: Task ${formattedTaskId} no longer depends on ${formattedDependencyId}` - ); + // Success message + log( + "success", + `Removed dependency: Task ${formattedTaskId} no longer depends on ${formattedDependencyId}` + ); - if (!isSilentMode()) { - // Display a more visually appealing success message - console.log( - boxen( - chalk.green(`Successfully removed dependency:\n\n`) + - `Task ${chalk.bold(formattedTaskId)} no longer depends on ${chalk.bold(formattedDependencyId)}`, - { - padding: 1, - borderColor: 'green', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); - } + if (!isSilentMode()) { + // Display a more visually appealing success message + console.log( + boxen( + chalk.green(`Successfully removed dependency:\n\n`) + + `Task ${chalk.bold(formattedTaskId)} no longer depends on ${chalk.bold(formattedDependencyId)}`, + { + padding: 1, + borderColor: "green", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); + } - // Regenerate task files - await generateTaskFiles(tasksPath, path.dirname(tasksPath)); + // Regenerate task files + await generateTaskFiles(tasksPath, path.dirname(tasksPath)); } /** @@ -345,54 +345,54 @@ async function removeDependency(tasksPath, taskId, dependencyId) { * @returns {boolean} True if circular dependency would be created */ function isCircularDependency(tasks, taskId, chain = []) { - // Convert taskId to string for comparison - const taskIdStr = String(taskId); + // Convert taskId to string for comparison + const taskIdStr = String(taskId); - // If we've seen this task before in the chain, we have a circular dependency - if (chain.some((id) => String(id) === taskIdStr)) { - return true; - } + // If we've seen this task before in the chain, we have a circular dependency + if (chain.some((id) => String(id) === taskIdStr)) { + return true; + } - // Find the task or subtask - let task = null; - let parentIdForSubtask = null; + // Find the task or subtask + let task = null; + let parentIdForSubtask = null; - // Check if this is a subtask reference (e.g., "1.2") - 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 + // Check if this is a subtask reference (e.g., "1.2") + 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 - if (parentTask && parentTask.subtasks) { - task = parentTask.subtasks.find((st) => st.id === subtaskId); - } - } else { - // Regular task - task = tasks.find((t) => String(t.id) === taskIdStr); - } + if (parentTask && parentTask.subtasks) { + task = parentTask.subtasks.find((st) => st.id === subtaskId); + } + } else { + // Regular task + task = tasks.find((t) => String(t.id) === taskIdStr); + } - if (!task) { - return false; // Task doesn't exist, can't create circular dependency - } + if (!task) { + return false; // Task doesn't exist, can't create circular dependency + } - // No dependencies, can't create circular dependency - if (!task.dependencies || task.dependencies.length === 0) { - return false; - } + // No dependencies, can't create circular dependency + if (!task.dependencies || task.dependencies.length === 0) { + return false; + } - // Check each dependency recursively - const newChain = [...chain, taskIdStr]; // Use taskIdStr for consistency - return task.dependencies.some((depId) => { - let normalizedDepId = String(depId); - // Normalize relative subtask dependencies - 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}`; - } - // Pass the normalized ID to the recursive call - return isCircularDependency(tasks, normalizedDepId, newChain); - }); + // Check each dependency recursively + const newChain = [...chain, taskIdStr]; // Use taskIdStr for consistency + return task.dependencies.some((depId) => { + let normalizedDepId = String(depId); + // Normalize relative subtask dependencies + 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}`; + } + // Pass the normalized ID to the recursive call + return isCircularDependency(tasks, normalizedDepId, newChain); + }); } /** @@ -401,96 +401,96 @@ function isCircularDependency(tasks, taskId, chain = []) { * @returns {Object} Validation result with valid flag and issues array */ function validateTaskDependencies(tasks) { - const issues = []; + const issues = []; - // Check each task's dependencies - tasks.forEach((task) => { - if (!task.dependencies) { - return; // No dependencies to validate - } + // Check each task's dependencies + tasks.forEach((task) => { + if (!task.dependencies) { + return; // No dependencies to validate + } - task.dependencies.forEach((depId) => { - // Check for self-dependencies - if (String(depId) === String(task.id)) { - issues.push({ - type: 'self', - taskId: task.id, - message: `Task ${task.id} depends on itself` - }); - return; - } + task.dependencies.forEach((depId) => { + // Check for self-dependencies + if (String(depId) === String(task.id)) { + issues.push({ + type: "self", + taskId: task.id, + message: `Task ${task.id} depends on itself`, + }); + return; + } - // Check if dependency exists - if (!taskExists(tasks, depId)) { - issues.push({ - type: 'missing', - taskId: task.id, - dependencyId: depId, - message: `Task ${task.id} depends on non-existent task ${depId}` - }); - } - }); + // Check if dependency exists + if (!taskExists(tasks, depId)) { + issues.push({ + type: "missing", + taskId: task.id, + dependencyId: depId, + message: `Task ${task.id} depends on non-existent task ${depId}`, + }); + } + }); - // Check for circular dependencies - if (isCircularDependency(tasks, task.id)) { - issues.push({ - type: 'circular', - taskId: task.id, - message: `Task ${task.id} is part of a circular dependency chain` - }); - } + // Check for circular dependencies + if (isCircularDependency(tasks, task.id)) { + issues.push({ + type: "circular", + taskId: task.id, + message: `Task ${task.id} is part of a circular dependency chain`, + }); + } - // Check subtask dependencies if they exist - if (task.subtasks && task.subtasks.length > 0) { - task.subtasks.forEach((subtask) => { - if (!subtask.dependencies) { - return; // No dependencies to validate - } + // Check subtask dependencies if they exist + if (task.subtasks && task.subtasks.length > 0) { + task.subtasks.forEach((subtask) => { + if (!subtask.dependencies) { + return; // No dependencies to validate + } - // Create a full subtask ID for reference - const fullSubtaskId = `${task.id}.${subtask.id}`; + // Create a full subtask ID for reference + const fullSubtaskId = `${task.id}.${subtask.id}`; - subtask.dependencies.forEach((depId) => { - // Check for self-dependencies in subtasks - if ( - String(depId) === String(fullSubtaskId) || - (typeof depId === 'number' && depId === subtask.id) - ) { - issues.push({ - type: 'self', - taskId: fullSubtaskId, - message: `Subtask ${fullSubtaskId} depends on itself` - }); - return; - } + subtask.dependencies.forEach((depId) => { + // Check for self-dependencies in subtasks + if ( + String(depId) === String(fullSubtaskId) || + (typeof depId === "number" && depId === subtask.id) + ) { + issues.push({ + type: "self", + taskId: fullSubtaskId, + message: `Subtask ${fullSubtaskId} depends on itself`, + }); + return; + } - // Check if dependency exists - if (!taskExists(tasks, depId)) { - issues.push({ - type: 'missing', - taskId: fullSubtaskId, - dependencyId: depId, - message: `Subtask ${fullSubtaskId} depends on non-existent task/subtask ${depId}` - }); - } - }); + // Check if dependency exists + if (!taskExists(tasks, depId)) { + issues.push({ + type: "missing", + taskId: fullSubtaskId, + dependencyId: depId, + message: `Subtask ${fullSubtaskId} depends on non-existent task/subtask ${depId}`, + }); + } + }); - // Check for circular dependencies in subtasks - if (isCircularDependency(tasks, fullSubtaskId)) { - issues.push({ - type: 'circular', - taskId: fullSubtaskId, - message: `Subtask ${fullSubtaskId} is part of a circular dependency chain` - }); - } - }); - } - }); + // Check for circular dependencies in subtasks + if (isCircularDependency(tasks, fullSubtaskId)) { + issues.push({ + type: "circular", + taskId: fullSubtaskId, + message: `Subtask ${fullSubtaskId} is part of a circular dependency chain`, + }); + } + }); + } + }); - return { - valid: issues.length === 0, - issues - }; + return { + valid: issues.length === 0, + issues, + }; } /** @@ -499,23 +499,23 @@ function validateTaskDependencies(tasks) { * @returns {Object} Updated tasks data with duplicates removed */ function removeDuplicateDependencies(tasksData) { - const tasks = tasksData.tasks.map((task) => { - if (!task.dependencies) { - return task; - } + const tasks = tasksData.tasks.map((task) => { + if (!task.dependencies) { + return task; + } - // Convert to Set and back to array to remove duplicates - const uniqueDeps = [...new Set(task.dependencies)]; - return { - ...task, - dependencies: uniqueDeps - }; - }); + // Convert to Set and back to array to remove duplicates + const uniqueDeps = [...new Set(task.dependencies)]; + return { + ...task, + dependencies: uniqueDeps, + }; + }); - return { - ...tasksData, - tasks - }; + return { + ...tasksData, + tasks, + }; } /** @@ -524,38 +524,38 @@ function removeDuplicateDependencies(tasksData) { * @returns {Object} Updated tasks data with invalid subtask dependencies removed */ function cleanupSubtaskDependencies(tasksData) { - const tasks = tasksData.tasks.map((task) => { - // Handle task's own dependencies - if (task.dependencies) { - task.dependencies = task.dependencies.filter((depId) => { - // Keep only dependencies that exist - return taskExists(tasksData.tasks, depId); - }); - } + const tasks = tasksData.tasks.map((task) => { + // Handle task's own dependencies + if (task.dependencies) { + task.dependencies = task.dependencies.filter((depId) => { + // Keep only dependencies that exist + return taskExists(tasksData.tasks, depId); + }); + } - // Handle subtask dependencies - if (task.subtasks) { - task.subtasks = task.subtasks.map((subtask) => { - if (!subtask.dependencies) { - return subtask; - } + // Handle subtask dependencies + if (task.subtasks) { + task.subtasks = task.subtasks.map((subtask) => { + if (!subtask.dependencies) { + return subtask; + } - // Filter out dependencies to non-existent subtasks - subtask.dependencies = subtask.dependencies.filter((depId) => { - return taskExists(tasksData.tasks, depId); - }); + // Filter out dependencies to non-existent subtasks + subtask.dependencies = subtask.dependencies.filter((depId) => { + return taskExists(tasksData.tasks, depId); + }); - return subtask; - }); - } + return subtask; + }); + } - return task; - }); + return task; + }); - return { - ...tasksData, - tasks - }; + return { + ...tasksData, + tasks, + }; } /** @@ -563,99 +563,94 @@ 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"); + process.exit(1); + } - // Read tasks data - const data = readJSON(tasksPath); - if (!data || !data.tasks) { - log('error', 'No valid tasks found in tasks.json'); - process.exit(1); - } + // Count of tasks and subtasks for reporting + const taskCount = data.tasks.length; + let subtaskCount = 0; + data.tasks.forEach((task) => { + if (task.subtasks && Array.isArray(task.subtasks)) { + subtaskCount += task.subtasks.length; + } + }); - // Count of tasks and subtasks for reporting - const taskCount = data.tasks.length; - let subtaskCount = 0; - data.tasks.forEach((task) => { - if (task.subtasks && Array.isArray(task.subtasks)) { - subtaskCount += task.subtasks.length; - } - }); + log( + "info", + `Analyzing dependencies for ${taskCount} tasks and ${subtaskCount} subtasks...` + ); - log( - 'info', - `Analyzing dependencies for ${taskCount} tasks and ${subtaskCount} subtasks...` - ); + try { + // Directly call the validation function + const validationResult = validateTaskDependencies(data.tasks); - try { - // Directly call the validation function - const validationResult = validateTaskDependencies(data.tasks); + if (!validationResult.valid) { + log( + "error", + `Dependency validation failed. Found ${validationResult.issues.length} issue(s):` + ); + validationResult.issues.forEach((issue) => { + let errorMsg = ` [${issue.type.toUpperCase()}] Task ${issue.taskId}: ${issue.message}`; + if (issue.dependencyId) { + errorMsg += ` (Dependency: ${issue.dependencyId})`; + } + log("error", errorMsg); // Log each issue as an error + }); - if (!validationResult.valid) { - log( - 'error', - `Dependency validation failed. Found ${validationResult.issues.length} issue(s):` - ); - validationResult.issues.forEach((issue) => { - let errorMsg = ` [${issue.type.toUpperCase()}] Task ${issue.taskId}: ${issue.message}`; - if (issue.dependencyId) { - errorMsg += ` (Dependency: ${issue.dependencyId})`; - } - log('error', errorMsg); // Log each issue as an error - }); + // Optionally exit if validation fails, depending on desired behavior + // process.exit(1); // Uncomment if validation failure should stop the process - // Optionally exit if validation fails, depending on desired behavior - // process.exit(1); // Uncomment if validation failure should stop the process + // Display summary box even on failure, showing issues found + if (!isSilentMode()) { + 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 + { + padding: 1, + borderColor: "red", + borderStyle: "round", + margin: { top: 1, bottom: 1 }, + } + ) + ); + } + } else { + log( + "success", + "No invalid dependencies found - all dependencies are valid" + ); - // Display summary box even on failure, showing issues found - if (!isSilentMode()) { - 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 - { - padding: 1, - borderColor: 'red', - borderStyle: 'round', - margin: { top: 1, bottom: 1 } - } - ) - ); - } - } else { - log( - 'success', - 'No invalid dependencies found - all dependencies are valid' - ); - - // Show validation summary - only if not in silent mode - if (!isSilentMode()) { - 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)}`, - { - padding: 1, - borderColor: 'green', - borderStyle: 'round', - margin: { top: 1, bottom: 1 } - } - ) - ); - } - } - } catch (error) { - log('error', 'Error validating dependencies:', error); - process.exit(1); - } + // Show validation summary - only if not in silent mode + if (!isSilentMode()) { + 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)}`, + { + padding: 1, + borderColor: "green", + borderStyle: "round", + margin: { top: 1, bottom: 1 }, + } + ) + ); + } + } + } catch (error) { + log("error", "Error validating dependencies:", error); + process.exit(1); + } } /** @@ -664,25 +659,25 @@ async function validateDependenciesCommand(tasksPath, options = {}) { * @returns {number} - Total number of dependencies */ function countAllDependencies(tasks) { - let count = 0; + let count = 0; - tasks.forEach((task) => { - // Count main task dependencies - if (task.dependencies && Array.isArray(task.dependencies)) { - count += task.dependencies.length; - } + tasks.forEach((task) => { + // Count main task dependencies + if (task.dependencies && Array.isArray(task.dependencies)) { + count += task.dependencies.length; + } - // Count subtask dependencies - if (task.subtasks && Array.isArray(task.subtasks)) { - task.subtasks.forEach((subtask) => { - if (subtask.dependencies && Array.isArray(subtask.dependencies)) { - count += subtask.dependencies.length; - } - }); - } - }); + // Count subtask dependencies + if (task.subtasks && Array.isArray(task.subtasks)) { + task.subtasks.forEach((subtask) => { + if (subtask.dependencies && Array.isArray(subtask.dependencies)) { + count += subtask.dependencies.length; + } + }); + } + }); - return count; + return count; } /** @@ -691,392 +686,387 @@ 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"); + process.exit(1); + } - try { - // Read tasks data - const data = readJSON(tasksPath); - if (!data || !data.tasks) { - log('error', 'No valid tasks found in tasks.json'); - process.exit(1); - } + // Create a deep copy of the original data for comparison + const originalData = JSON.parse(JSON.stringify(data)); - // Create a deep copy of the original data for comparison - const originalData = JSON.parse(JSON.stringify(data)); + // Track fixes for reporting + const stats = { + nonExistentDependenciesRemoved: 0, + selfDependenciesRemoved: 0, + duplicateDependenciesRemoved: 0, + circularDependenciesFixed: 0, + tasksFixed: 0, + subtasksFixed: 0, + }; - // Track fixes for reporting - const stats = { - nonExistentDependenciesRemoved: 0, - selfDependenciesRemoved: 0, - duplicateDependenciesRemoved: 0, - circularDependenciesFixed: 0, - tasksFixed: 0, - subtasksFixed: 0 - }; + // First phase: Remove duplicate dependencies in tasks + data.tasks.forEach((task) => { + if (task.dependencies && Array.isArray(task.dependencies)) { + const uniqueDeps = new Set(); + const originalLength = task.dependencies.length; + task.dependencies = task.dependencies.filter((depId) => { + const depIdStr = String(depId); + if (uniqueDeps.has(depIdStr)) { + log( + "info", + `Removing duplicate dependency from task ${task.id}: ${depId}` + ); + stats.duplicateDependenciesRemoved++; + return false; + } + uniqueDeps.add(depIdStr); + return true; + }); + if (task.dependencies.length < originalLength) { + stats.tasksFixed++; + } + } - // First phase: Remove duplicate dependencies in tasks - data.tasks.forEach((task) => { - if (task.dependencies && Array.isArray(task.dependencies)) { - const uniqueDeps = new Set(); - const originalLength = task.dependencies.length; - task.dependencies = task.dependencies.filter((depId) => { - const depIdStr = String(depId); - if (uniqueDeps.has(depIdStr)) { - log( - 'info', - `Removing duplicate dependency from task ${task.id}: ${depId}` - ); - stats.duplicateDependenciesRemoved++; - return false; - } - uniqueDeps.add(depIdStr); - return true; - }); - if (task.dependencies.length < originalLength) { - stats.tasksFixed++; - } - } + // Check for duplicates in subtasks + if (task.subtasks && Array.isArray(task.subtasks)) { + task.subtasks.forEach((subtask) => { + if (subtask.dependencies && Array.isArray(subtask.dependencies)) { + const uniqueDeps = new Set(); + const originalLength = subtask.dependencies.length; + subtask.dependencies = subtask.dependencies.filter((depId) => { + let depIdStr = String(depId); + if (typeof depId === "number" && depId < 100) { + depIdStr = `${task.id}.${depId}`; + } + if (uniqueDeps.has(depIdStr)) { + log( + "info", + `Removing duplicate dependency from subtask ${task.id}.${subtask.id}: ${depId}` + ); + stats.duplicateDependenciesRemoved++; + return false; + } + uniqueDeps.add(depIdStr); + return true; + }); + if (subtask.dependencies.length < originalLength) { + stats.subtasksFixed++; + } + } + }); + } + }); - // Check for duplicates in subtasks - if (task.subtasks && Array.isArray(task.subtasks)) { - task.subtasks.forEach((subtask) => { - if (subtask.dependencies && Array.isArray(subtask.dependencies)) { - const uniqueDeps = new Set(); - const originalLength = subtask.dependencies.length; - subtask.dependencies = subtask.dependencies.filter((depId) => { - let depIdStr = String(depId); - if (typeof depId === 'number' && depId < 100) { - depIdStr = `${task.id}.${depId}`; - } - if (uniqueDeps.has(depIdStr)) { - log( - 'info', - `Removing duplicate dependency from subtask ${task.id}.${subtask.id}: ${depId}` - ); - stats.duplicateDependenciesRemoved++; - return false; - } - uniqueDeps.add(depIdStr); - return true; - }); - if (subtask.dependencies.length < originalLength) { - stats.subtasksFixed++; - } - } - }); - } - }); + // Create validity maps for tasks and subtasks + const validTaskIds = new Set(data.tasks.map((t) => t.id)); + const validSubtaskIds = new Set(); + data.tasks.forEach((task) => { + if (task.subtasks && Array.isArray(task.subtasks)) { + task.subtasks.forEach((subtask) => { + validSubtaskIds.add(`${task.id}.${subtask.id}`); + }); + } + }); - // Create validity maps for tasks and subtasks - const validTaskIds = new Set(data.tasks.map((t) => t.id)); - const validSubtaskIds = new Set(); - data.tasks.forEach((task) => { - if (task.subtasks && Array.isArray(task.subtasks)) { - task.subtasks.forEach((subtask) => { - validSubtaskIds.add(`${task.id}.${subtask.id}`); - }); - } - }); + // Second phase: Remove invalid task dependencies (non-existent tasks) + data.tasks.forEach((task) => { + 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("."); - // Second phase: Remove invalid task dependencies (non-existent tasks) - data.tasks.forEach((task) => { - 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('.'); + if (isSubtask) { + // Check if the subtask exists + if (!validSubtaskIds.has(depId)) { + log( + "info", + `Removing invalid subtask dependency from task ${task.id}: ${depId} (subtask does not exist)` + ); + stats.nonExistentDependenciesRemoved++; + return false; + } + return true; + } else { + // Check if the task exists + const numericId = + typeof depId === "string" ? parseInt(depId, 10) : depId; + if (!validTaskIds.has(numericId)) { + log( + "info", + `Removing invalid task dependency from task ${task.id}: ${depId} (task does not exist)` + ); + stats.nonExistentDependenciesRemoved++; + return false; + } + return true; + } + }); - if (isSubtask) { - // Check if the subtask exists - if (!validSubtaskIds.has(depId)) { - log( - 'info', - `Removing invalid subtask dependency from task ${task.id}: ${depId} (subtask does not exist)` - ); - stats.nonExistentDependenciesRemoved++; - return false; - } - return true; - } else { - // Check if the task exists - const numericId = - typeof depId === 'string' ? parseInt(depId, 10) : depId; - if (!validTaskIds.has(numericId)) { - log( - 'info', - `Removing invalid task dependency from task ${task.id}: ${depId} (task does not exist)` - ); - stats.nonExistentDependenciesRemoved++; - return false; - } - return true; - } - }); + if (task.dependencies.length < originalLength) { + stats.tasksFixed++; + } + } - if (task.dependencies.length < originalLength) { - stats.tasksFixed++; - } - } + // Check subtask dependencies for invalid references + if (task.subtasks && Array.isArray(task.subtasks)) { + task.subtasks.forEach((subtask) => { + if (subtask.dependencies && Array.isArray(subtask.dependencies)) { + const originalLength = subtask.dependencies.length; + const subtaskId = `${task.id}.${subtask.id}`; - // Check subtask dependencies for invalid references - if (task.subtasks && Array.isArray(task.subtasks)) { - task.subtasks.forEach((subtask) => { - if (subtask.dependencies && Array.isArray(subtask.dependencies)) { - const originalLength = subtask.dependencies.length; - const subtaskId = `${task.id}.${subtask.id}`; + // First check for self-dependencies + const hasSelfDependency = subtask.dependencies.some((depId) => { + if (typeof depId === "string" && depId.includes(".")) { + return depId === subtaskId; + } else if (typeof depId === "number" && depId < 100) { + return depId === subtask.id; + } + return false; + }); - // First check for self-dependencies - const hasSelfDependency = subtask.dependencies.some((depId) => { - if (typeof depId === 'string' && depId.includes('.')) { - return depId === subtaskId; - } else if (typeof depId === 'number' && depId < 100) { - return depId === subtask.id; - } - return false; - }); + if (hasSelfDependency) { + subtask.dependencies = subtask.dependencies.filter((depId) => { + const normalizedDepId = + typeof depId === "number" && depId < 100 + ? `${task.id}.${depId}` + : String(depId); - if (hasSelfDependency) { - subtask.dependencies = subtask.dependencies.filter((depId) => { - const normalizedDepId = - typeof depId === 'number' && depId < 100 - ? `${task.id}.${depId}` - : String(depId); + if (normalizedDepId === subtaskId) { + log( + "info", + `Removing self-dependency from subtask ${subtaskId}` + ); + stats.selfDependenciesRemoved++; + return false; + } + return true; + }); + } - if (normalizedDepId === subtaskId) { - log( - 'info', - `Removing self-dependency from subtask ${subtaskId}` - ); - stats.selfDependenciesRemoved++; - return false; - } - return true; - }); - } + // Then check for non-existent dependencies + subtask.dependencies = subtask.dependencies.filter((depId) => { + if (typeof depId === "string" && depId.includes(".")) { + if (!validSubtaskIds.has(depId)) { + log( + "info", + `Removing invalid subtask dependency from subtask ${subtaskId}: ${depId} (subtask does not exist)` + ); + stats.nonExistentDependenciesRemoved++; + return false; + } + return true; + } - // Then check for non-existent dependencies - subtask.dependencies = subtask.dependencies.filter((depId) => { - if (typeof depId === 'string' && depId.includes('.')) { - if (!validSubtaskIds.has(depId)) { - log( - 'info', - `Removing invalid subtask dependency from subtask ${subtaskId}: ${depId} (subtask does not exist)` - ); - stats.nonExistentDependenciesRemoved++; - return false; - } - return true; - } + // Handle numeric dependencies + const numericId = + typeof depId === "number" ? depId : parseInt(depId, 10); - // Handle numeric dependencies - const numericId = - typeof depId === 'number' ? depId : parseInt(depId, 10); + // Small numbers likely refer to subtasks in the same task + if (numericId < 100) { + const fullSubtaskId = `${task.id}.${numericId}`; - // Small numbers likely refer to subtasks in the same task - if (numericId < 100) { - const fullSubtaskId = `${task.id}.${numericId}`; + if (!validSubtaskIds.has(fullSubtaskId)) { + log( + "info", + `Removing invalid subtask dependency from subtask ${subtaskId}: ${numericId}` + ); + stats.nonExistentDependenciesRemoved++; + return false; + } - if (!validSubtaskIds.has(fullSubtaskId)) { - log( - 'info', - `Removing invalid subtask dependency from subtask ${subtaskId}: ${numericId}` - ); - stats.nonExistentDependenciesRemoved++; - return false; - } + return true; + } - return true; - } + // Otherwise it's a task reference + if (!validTaskIds.has(numericId)) { + log( + "info", + `Removing invalid task dependency from subtask ${subtaskId}: ${numericId}` + ); + stats.nonExistentDependenciesRemoved++; + return false; + } - // Otherwise it's a task reference - if (!validTaskIds.has(numericId)) { - log( - 'info', - `Removing invalid task dependency from subtask ${subtaskId}: ${numericId}` - ); - stats.nonExistentDependenciesRemoved++; - return false; - } + return true; + }); - return true; - }); + if (subtask.dependencies.length < originalLength) { + stats.subtasksFixed++; + } + } + }); + } + }); - if (subtask.dependencies.length < originalLength) { - stats.subtasksFixed++; - } - } - }); - } - }); + // Third phase: Check for circular dependencies + log("info", "Checking for circular dependencies..."); - // Third phase: Check for circular dependencies - log('info', 'Checking for circular dependencies...'); + // Build the dependency map for subtasks + const subtaskDependencyMap = new Map(); + data.tasks.forEach((task) => { + if (task.subtasks && Array.isArray(task.subtasks)) { + task.subtasks.forEach((subtask) => { + const subtaskId = `${task.id}.${subtask.id}`; - // Build the dependency map for subtasks - const subtaskDependencyMap = new Map(); - data.tasks.forEach((task) => { - if (task.subtasks && Array.isArray(task.subtasks)) { - task.subtasks.forEach((subtask) => { - const subtaskId = `${task.id}.${subtask.id}`; + if (subtask.dependencies && Array.isArray(subtask.dependencies)) { + const normalizedDeps = subtask.dependencies.map((depId) => { + if (typeof depId === "string" && depId.includes(".")) { + return depId; + } else if (typeof depId === "number" && depId < 100) { + return `${task.id}.${depId}`; + } + return String(depId); + }); + subtaskDependencyMap.set(subtaskId, normalizedDeps); + } else { + subtaskDependencyMap.set(subtaskId, []); + } + }); + } + }); - if (subtask.dependencies && Array.isArray(subtask.dependencies)) { - const normalizedDeps = subtask.dependencies.map((depId) => { - if (typeof depId === 'string' && depId.includes('.')) { - return depId; - } else if (typeof depId === 'number' && depId < 100) { - return `${task.id}.${depId}`; - } - return String(depId); - }); - subtaskDependencyMap.set(subtaskId, normalizedDeps); - } else { - subtaskDependencyMap.set(subtaskId, []); - } - }); - } - }); + // Check for and fix circular dependencies + for (const [subtaskId, dependencies] of subtaskDependencyMap.entries()) { + const visited = new Set(); + const recursionStack = new Set(); - // Check for and fix circular dependencies - for (const [subtaskId, dependencies] of subtaskDependencyMap.entries()) { - const visited = new Set(); - const recursionStack = new Set(); + // Detect cycles + const cycleEdges = findCycles( + subtaskId, + subtaskDependencyMap, + visited, + recursionStack + ); - // Detect cycles - const cycleEdges = findCycles( - subtaskId, - subtaskDependencyMap, - visited, - recursionStack - ); + if (cycleEdges.length > 0) { + const [taskId, subtaskNum] = subtaskId + .split(".") + .map((part) => Number(part)); + const task = data.tasks.find((t) => t.id === taskId); - if (cycleEdges.length > 0) { - const [taskId, subtaskNum] = subtaskId - .split('.') - .map((part) => Number(part)); - const task = data.tasks.find((t) => t.id === taskId); + if (task && task.subtasks) { + const subtask = task.subtasks.find((st) => st.id === subtaskNum); - if (task && task.subtasks) { - const subtask = task.subtasks.find((st) => st.id === subtaskNum); + if (subtask && subtask.dependencies) { + const originalLength = subtask.dependencies.length; - if (subtask && subtask.dependencies) { - const originalLength = subtask.dependencies.length; + const edgesToRemove = cycleEdges.map((edge) => { + if (edge.includes(".")) { + const [depTaskId, depSubtaskId] = edge + .split(".") + .map((part) => Number(part)); - const edgesToRemove = cycleEdges.map((edge) => { - if (edge.includes('.')) { - const [depTaskId, depSubtaskId] = edge - .split('.') - .map((part) => Number(part)); + if (depTaskId === taskId) { + return depSubtaskId; + } - if (depTaskId === taskId) { - return depSubtaskId; - } + return edge; + } - return edge; - } + return Number(edge); + }); - return Number(edge); - }); + subtask.dependencies = subtask.dependencies.filter((depId) => { + const normalizedDepId = + typeof depId === "number" && depId < 100 + ? `${taskId}.${depId}` + : String(depId); - subtask.dependencies = subtask.dependencies.filter((depId) => { - const normalizedDepId = - typeof depId === 'number' && depId < 100 - ? `${taskId}.${depId}` - : String(depId); + if ( + edgesToRemove.includes(depId) || + edgesToRemove.includes(normalizedDepId) + ) { + log( + "info", + `Breaking circular dependency: Removing ${normalizedDepId} from subtask ${subtaskId}` + ); + stats.circularDependenciesFixed++; + return false; + } + return true; + }); - if ( - edgesToRemove.includes(depId) || - edgesToRemove.includes(normalizedDepId) - ) { - log( - 'info', - `Breaking circular dependency: Removing ${normalizedDepId} from subtask ${subtaskId}` - ); - stats.circularDependenciesFixed++; - return false; - } - return true; - }); + if (subtask.dependencies.length < originalLength) { + stats.subtasksFixed++; + } + } + } + } + } - if (subtask.dependencies.length < originalLength) { - stats.subtasksFixed++; - } - } - } - } - } + // Check if any changes were made by comparing with original data + const dataChanged = JSON.stringify(data) !== JSON.stringify(originalData); - // Check if any changes were made by comparing with original data - const dataChanged = JSON.stringify(data) !== JSON.stringify(originalData); + if (dataChanged) { + // Save the changes + writeJSON(tasksPath, data); + log("success", "Fixed dependency issues in tasks.json"); - if (dataChanged) { - // Save the changes - writeJSON(tasksPath, data); - log('success', 'Fixed dependency issues in tasks.json'); + // Regenerate task files + log("info", "Regenerating task files to reflect dependency changes..."); + await generateTaskFiles(tasksPath, path.dirname(tasksPath)); + } else { + log("info", "No changes needed to fix dependencies"); + } - // Regenerate task files - log('info', 'Regenerating task files to reflect dependency changes...'); - await generateTaskFiles(tasksPath, path.dirname(tasksPath)); - } else { - log('info', 'No changes needed to fix dependencies'); - } + // Show detailed statistics report + const totalFixedAll = + stats.nonExistentDependenciesRemoved + + stats.selfDependenciesRemoved + + stats.duplicateDependenciesRemoved + + stats.circularDependenciesFixed; - // Show detailed statistics report - const totalFixedAll = - stats.nonExistentDependenciesRemoved + - stats.selfDependenciesRemoved + - stats.duplicateDependenciesRemoved + - stats.circularDependenciesFixed; + if (!isSilentMode()) { + if (totalFixedAll > 0) { + log("success", `Fixed ${totalFixedAll} dependency issues in total!`); - if (!isSilentMode()) { - if (totalFixedAll > 0) { - 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`, + { + padding: 1, + borderColor: "green", + borderStyle: "round", + margin: { top: 1, bottom: 1 }, + } + ) + ); + } else { + log( + "success", + "No dependency issues found - all dependencies are valid" + ); - 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`, - { - padding: 1, - borderColor: 'green', - borderStyle: 'round', - margin: { top: 1, bottom: 1 } - } - ) - ); - } else { - log( - '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)}`, - { - padding: 1, - borderColor: 'green', - borderStyle: 'round', - margin: { top: 1, bottom: 1 } - } - ) - ); - } - } - } catch (error) { - log('error', 'Error in fix-dependencies command:', error); - process.exit(1); - } + 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)}`, + { + padding: 1, + borderColor: "green", + borderStyle: "round", + margin: { top: 1, bottom: 1 }, + } + ) + ); + } + } + } catch (error) { + log("error", "Error in fix-dependencies command:", error); + process.exit(1); + } } /** @@ -1085,44 +1075,44 @@ async function fixDependenciesCommand(tasksPath, options = {}) { * @returns {boolean} - True if any changes were made */ function ensureAtLeastOneIndependentSubtask(tasksData) { - if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) { - return false; - } + if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) { + return false; + } - let changesDetected = false; + let changesDetected = false; - tasksData.tasks.forEach((task) => { - if ( - !task.subtasks || - !Array.isArray(task.subtasks) || - task.subtasks.length === 0 - ) { - return; - } + tasksData.tasks.forEach((task) => { + if ( + !task.subtasks || + !Array.isArray(task.subtasks) || + task.subtasks.length === 0 + ) { + return; + } - // Check if any subtask has no dependencies - const hasIndependentSubtask = task.subtasks.some( - (st) => - !st.dependencies || - !Array.isArray(st.dependencies) || - st.dependencies.length === 0 - ); + // Check if any subtask has no dependencies + const hasIndependentSubtask = task.subtasks.some( + (st) => + !st.dependencies || + !Array.isArray(st.dependencies) || + st.dependencies.length === 0 + ); - if (!hasIndependentSubtask) { - // Find the first subtask and clear its dependencies - if (task.subtasks.length > 0) { - const firstSubtask = task.subtasks[0]; - log( - 'debug', - `Ensuring at least one independent subtask: Clearing dependencies for subtask ${task.id}.${firstSubtask.id}` - ); - firstSubtask.dependencies = []; - changesDetected = true; - } - } - }); + if (!hasIndependentSubtask) { + // Find the first subtask and clear its dependencies + if (task.subtasks.length > 0) { + const firstSubtask = task.subtasks[0]; + log( + "debug", + `Ensuring at least one independent subtask: Clearing dependencies for subtask ${task.id}.${firstSubtask.id}` + ); + firstSubtask.dependencies = []; + changesDetected = true; + } + } + }); - return changesDetected; + return changesDetected; } /** @@ -1133,111 +1123,111 @@ function ensureAtLeastOneIndependentSubtask(tasksData) { * @returns {boolean} - True if any changes were made */ function validateAndFixDependencies(tasksData, tasksPath = null) { - if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) { - log('error', 'Invalid tasks data'); - return false; - } + if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) { + 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)); + // Create a deep copy for comparison + const originalData = JSON.parse(JSON.stringify(tasksData)); - // 1. Remove duplicate dependencies from tasks and subtasks - tasksData.tasks = tasksData.tasks.map((task) => { - // Handle task dependencies - if (task.dependencies) { - const uniqueDeps = [...new Set(task.dependencies)]; - task.dependencies = uniqueDeps; - } + // 1. Remove duplicate dependencies from tasks and subtasks + tasksData.tasks = tasksData.tasks.map((task) => { + // Handle task dependencies + if (task.dependencies) { + const uniqueDeps = [...new Set(task.dependencies)]; + task.dependencies = uniqueDeps; + } - // Handle subtask dependencies - if (task.subtasks) { - task.subtasks = task.subtasks.map((subtask) => { - if (subtask.dependencies) { - const uniqueDeps = [...new Set(subtask.dependencies)]; - subtask.dependencies = uniqueDeps; - } - return subtask; - }); - } - return task; - }); + // Handle subtask dependencies + if (task.subtasks) { + task.subtasks = task.subtasks.map((subtask) => { + if (subtask.dependencies) { + const uniqueDeps = [...new Set(subtask.dependencies)]; + subtask.dependencies = uniqueDeps; + } + return subtask; + }); + } + return task; + }); - // 2. Remove invalid task dependencies (non-existent tasks) - tasksData.tasks.forEach((task) => { - // Clean up task dependencies - if (task.dependencies) { - task.dependencies = task.dependencies.filter((depId) => { - // Remove self-dependencies - if (String(depId) === String(task.id)) { - return false; - } - // Remove non-existent dependencies - return taskExists(tasksData.tasks, depId); - }); - } + // 2. Remove invalid task dependencies (non-existent tasks) + tasksData.tasks.forEach((task) => { + // Clean up task dependencies + if (task.dependencies) { + task.dependencies = task.dependencies.filter((depId) => { + // Remove self-dependencies + if (String(depId) === String(task.id)) { + return false; + } + // Remove non-existent dependencies + return taskExists(tasksData.tasks, depId); + }); + } - // Clean up subtask dependencies - if (task.subtasks) { - task.subtasks.forEach((subtask) => { - if (subtask.dependencies) { - subtask.dependencies = subtask.dependencies.filter((depId) => { - // Handle numeric subtask references - if (typeof depId === 'number' && depId < 100) { - const fullSubtaskId = `${task.id}.${depId}`; - return taskExists(tasksData.tasks, fullSubtaskId); - } - // Handle full task/subtask references - return taskExists(tasksData.tasks, depId); - }); - } - }); - } - }); + // Clean up subtask dependencies + if (task.subtasks) { + task.subtasks.forEach((subtask) => { + if (subtask.dependencies) { + subtask.dependencies = subtask.dependencies.filter((depId) => { + // Handle numeric subtask references + if (typeof depId === "number" && depId < 100) { + const fullSubtaskId = `${task.id}.${depId}`; + return taskExists(tasksData.tasks, fullSubtaskId); + } + // Handle full task/subtask references + return taskExists(tasksData.tasks, depId); + }); + } + }); + } + }); - // 3. Ensure at least one subtask has no dependencies in each task - tasksData.tasks.forEach((task) => { - if (task.subtasks && task.subtasks.length > 0) { - const hasIndependentSubtask = task.subtasks.some( - (st) => - !st.dependencies || - !Array.isArray(st.dependencies) || - st.dependencies.length === 0 - ); + // 3. Ensure at least one subtask has no dependencies in each task + tasksData.tasks.forEach((task) => { + if (task.subtasks && task.subtasks.length > 0) { + const hasIndependentSubtask = task.subtasks.some( + (st) => + !st.dependencies || + !Array.isArray(st.dependencies) || + st.dependencies.length === 0 + ); - if (!hasIndependentSubtask) { - task.subtasks[0].dependencies = []; - } - } - }); + if (!hasIndependentSubtask) { + task.subtasks[0].dependencies = []; + } + } + }); - // Check if any changes were made by comparing with original data - const changesDetected = - JSON.stringify(tasksData) !== JSON.stringify(originalData); + // Check if any changes were made by comparing with original data + const changesDetected = + JSON.stringify(tasksData) !== JSON.stringify(originalData); - // Save changes if needed - if (tasksPath && changesDetected) { - try { - writeJSON(tasksPath, tasksData); - log('debug', 'Saved dependency fixes to tasks.json'); - } catch (error) { - log('error', 'Failed to save dependency fixes to tasks.json', error); - } - } + // Save changes if needed + if (tasksPath && changesDetected) { + try { + writeJSON(tasksPath, tasksData); + log("debug", "Saved dependency fixes to tasks.json"); + } catch (error) { + log("error", "Failed to save dependency fixes to tasks.json", error); + } + } - return changesDetected; + return changesDetected; } export { - addDependency, - removeDependency, - isCircularDependency, - validateTaskDependencies, - validateDependenciesCommand, - fixDependenciesCommand, - removeDuplicateDependencies, - cleanupSubtaskDependencies, - ensureAtLeastOneIndependentSubtask, - validateAndFixDependencies + addDependency, + removeDependency, + isCircularDependency, + validateTaskDependencies, + validateDependenciesCommand, + fixDependenciesCommand, + removeDuplicateDependencies, + cleanupSubtaskDependencies, + ensureAtLeastOneIndependentSubtask, + validateAndFixDependencies, }; diff --git a/scripts/modules/task-manager/add-task.js b/scripts/modules/task-manager/add-task.js index 1d0e13ec..e51e0884 100644 --- a/scripts/modules/task-manager/add-task.js +++ b/scripts/modules/task-manager/add-task.js @@ -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'; + displayBanner, + getStatusWithColor, + startLoadingIndicator, + stopLoadingIndicator, + 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'), - description: z - .string() - .describe('A one or two sentence description of the task'), - details: z - .string() - .describe('In-depth implementation details, considerations, and guidance'), - testStrategy: z - .string() - .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)' - ) + title: z.string().describe("Clear, concise title for the task"), + description: z + .string() + .describe("A one or two sentence description of the task"), + details: z + .string() + .describe("In-depth implementation details, considerations, and guidance"), + testStrategy: z + .string() + .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)" + ), }); /** @@ -57,843 +59,790 @@ const AiTaskDataSchema = z.object({ * @returns {Promise} An object containing newTaskId and telemetryData */ async function addTask( - tasksPath, - prompt, - dependencies = [], - priority = null, - context = {}, - outputFormat = 'text', // Default to text for CLI - manualTaskData = null, - useResearch = false + tasksPath, + prompt, + dependencies = [], + priority = null, + context = {}, + outputFormat = "text", // Default to text for CLI + manualTaskData = null, + useResearch = false ) { - const { session, mcpLog, projectRoot, commandName, outputType } = context; - const isMCP = !!mcpLog; - - // Create a consistent logFn object regardless of context - const logFn = isMCP - ? 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) - }; - - const effectivePriority = priority || getDefaultPriority(projectRoot); - - logFn.info( - `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') => { - if (mcpLog) { - mcpLog[level](message); - } else if (outputFormat === 'text') { - consoleLog(level, message); - } - }; - - /** - * Recursively builds a dependency graph for a given task - * @param {Array} tasks - All tasks from tasks.json - * @param {number} taskId - ID of the task to analyze - * @param {Set} visited - Set of already visited task IDs - * @param {Map} depthMap - Map of task ID to its depth in the graph - * @param {number} depth - Current depth in the recursion - * @return {Object} Dependency graph data - */ - function buildDependencyGraph( - tasks, - taskId, - visited = new Set(), - depthMap = new Map(), - depth = 0 - ) { - // Skip if we've already visited this task or it doesn't exist - if (visited.has(taskId)) { - return null; - } - - // Find the task - const task = tasks.find((t) => t.id === taskId); - if (!task) { - return null; - } - - // Mark as visited - visited.add(taskId); - - // Update depth if this is a deeper path to this task - if (!depthMap.has(taskId) || depth < depthMap.get(taskId)) { - depthMap.set(taskId, depth); - } - - // Process dependencies - const dependencyData = []; - if (task.dependencies && task.dependencies.length > 0) { - for (const depId of task.dependencies) { - const depData = buildDependencyGraph( - tasks, - depId, - visited, - depthMap, - depth + 1 - ); - if (depData) { - dependencyData.push(depData); - } - } - } - - return { - id: task.id, - title: task.title, - description: task.description, - status: task.status, - dependencies: dependencyData - }; - } - - try { - // Read the existing tasks - let data = readJSON(tasksPath); - - // 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'); - // Create default tasks data structure - data = { - tasks: [] - }; - // Ensure the directory exists and write the new file - writeJSON(tasksPath, data); - report('Created new tasks.json file with empty tasks array.', 'info'); - } - - // Find the highest task ID to determine the next ID - const highestId = - data.tasks.length > 0 ? Math.max(...data.tasks.map((t) => t.id)) : 0; - const newTaskId = highestId + 1; - - // Only show UI box for CLI mode - if (outputFormat === 'text') { - console.log( - boxen(chalk.white.bold(`Creating New Task #${newTaskId}`), { - padding: 1, - borderColor: 'blue', - borderStyle: 'round', - margin: { top: 1, bottom: 1 } - }) - ); - } - - // Validate dependencies before proceeding - const invalidDeps = dependencies.filter((depId) => { - // Ensure depId is parsed as a number for comparison - const numDepId = parseInt(depId, 10); - return isNaN(numDepId) || !data.tasks.some((t) => t.id === numDepId); - }); - - if (invalidDeps.length > 0) { - report( - `The following dependencies do not exist or are invalid: ${invalidDeps.join(', ')}`, - 'warn' - ); - report('Removing invalid dependencies...', 'info'); - dependencies = dependencies.filter( - (depId) => !invalidDeps.includes(depId) - ); - } - // Ensure dependencies are numbers - const numericDependencies = dependencies.map((dep) => parseInt(dep, 10)); - - // Build dependency graphs for explicitly specified dependencies - const dependencyGraphs = []; - const allRelatedTaskIds = new Set(); - const depthMap = new Map(); - - // First pass: build a complete dependency graph for each specified dependency - for (const depId of numericDependencies) { - const graph = buildDependencyGraph( - data.tasks, - depId, - new Set(), - depthMap - ); - if (graph) { - dependencyGraphs.push(graph); - } - } - - // Second pass: build a set of all related task IDs for flat analysis - for (const [taskId, depth] of depthMap.entries()) { - allRelatedTaskIds.add(taskId); - } - - let taskData; - - // Check if manual task data is provided - if (manualTaskData) { - report('Using manually provided task data', 'info'); - taskData = manualTaskData; - report('DEBUG: Taking MANUAL task data path.', 'debug'); - - // Basic validation for manual data - if ( - !taskData.title || - typeof taskData.title !== 'string' || - !taskData.description || - typeof taskData.description !== 'string' - ) { - throw new Error( - 'Manual task data must include at least a title and description.' - ); - } - } else { - report('DEBUG: Taking AI task generation path.', 'debug'); - // --- Refactored AI Interaction --- - report(`Generating task data with AI with prompt:\n${prompt}`, 'info'); - - // Create context string for task creation prompt - let contextTasks = ''; - - // Create a dependency map for better understanding of the task relationships - const taskMap = {}; - data.tasks.forEach((t) => { - // For each task, only include id, title, description, and dependencies - taskMap[t.id] = { - id: t.id, - title: t.title, - description: t.description, - dependencies: t.dependencies || [], - status: t.status - }; - }); - - // CLI-only feedback for the dependency analysis - if (outputFormat === 'text') { - console.log( - boxen(chalk.cyan.bold('Task Context Analysis') + '\n', { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - margin: { top: 0, bottom: 0 }, - borderColor: 'cyan', - borderStyle: 'round' - }) - ); - } - - // Initialize variables that will be used in either branch - let uniqueDetailedTasks = []; - let dependentTasks = []; - let promptCategory = null; - - if (numericDependencies.length > 0) { - // If specific dependencies were provided, focus on them - // Get all tasks that were found in the dependency graph - dependentTasks = Array.from(allRelatedTaskIds) - .map((id) => data.tasks.find((t) => t.id === id)) - .filter(Boolean); - - // Sort by depth in the dependency chain - dependentTasks.sort((a, b) => { - const depthA = depthMap.get(a.id) || 0; - const depthB = depthMap.get(b.id) || 0; - return depthA - depthB; // Lowest depth (root dependencies) first - }); - - // Limit the number of detailed tasks to avoid context explosion - uniqueDetailedTasks = dependentTasks.slice(0, 8); - - contextTasks = `\nThis task relates to a dependency structure with ${dependentTasks.length} related tasks in the chain.\n\nDirect dependencies:`; - const directDeps = data.tasks.filter((t) => - numericDependencies.includes(t.id) - ); - 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( - (t) => !numericDependencies.includes(t.id) - ); - if (indirectDeps.length > 0) { - contextTasks += `\n\nIndirect dependencies (dependencies of dependencies):`; - contextTasks += `\n${indirectDeps - .slice(0, 5) - .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) - .join('\n')}`; - if (indirectDeps.length > 5) { - contextTasks += `\n- ... and ${indirectDeps.length - 5} more indirect dependencies`; - } - } - - // Add more details about each dependency, prioritizing direct dependencies - contextTasks += `\n\nDetailed information about dependencies:`; - for (const depTask of uniqueDetailedTasks) { - const depthInfo = depthMap.get(depTask.id) - ? ` (depth: ${depthMap.get(depTask.id)})` - : ''; - const isDirect = numericDependencies.includes(depTask.id) - ? ' [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`; - - // List its dependencies - if (depTask.dependencies && depTask.dependencies.length > 0) { - const depDeps = depTask.dependencies.map((dId) => { - const depDepTask = data.tasks.find((t) => t.id === dId); - return depDepTask - ? `Task ${dId}: ${depDepTask.title}` - : `Task ${dId}`; - }); - contextTasks += `Dependencies: ${depDeps.join(', ')}\n`; - } else { - contextTasks += `Dependencies: None\n`; - } - - // Add implementation details but truncate if too long - if (depTask.details) { - const truncatedDetails = - depTask.details.length > 400 - ? depTask.details.substring(0, 400) + '... (truncated)' - : depTask.details; - contextTasks += `Implementation Details: ${truncatedDetails}\n`; - } - } - - // Add dependency chain visualization - if (dependencyGraphs.length > 0) { - contextTasks += '\n\nDependency Chain Visualization:'; - - // Helper function to format dependency chain as text - function formatDependencyChain( - node, - prefix = '', - isLast = true, - depth = 0 - ) { - if (depth > 3) return ''; // Limit depth to avoid excessive nesting - - const connector = isLast ? '└── ' : '├── '; - const childPrefix = isLast ? ' ' : '│ '; - - let result = `\n${prefix}${connector}Task ${node.id}: ${node.title}`; - - if (node.dependencies && node.dependencies.length > 0) { - for (let i = 0; i < node.dependencies.length; i++) { - const isLastChild = i === node.dependencies.length - 1; - result += formatDependencyChain( - node.dependencies[i], - prefix + childPrefix, - isLastChild, - depth + 1 - ); - } - } - - return result; - } - - // Format each dependency graph - for (const graph of dependencyGraphs) { - contextTasks += formatDependencyChain(graph); - } - } - - // Show dependency analysis in CLI mode - if (outputFormat === 'text') { - if (directDeps.length > 0) { - console.log(chalk.gray(` Explicitly specified dependencies:`)); - directDeps.forEach((t) => { - console.log( - chalk.yellow(` • Task ${t.id}: ${truncate(t.title, 50)}`) - ); - }); - } - - if (indirectDeps.length > 0) { - console.log( - chalk.gray( - `\n Indirect dependencies (${indirectDeps.length} total):` - ) - ); - indirectDeps.slice(0, 3).forEach((t) => { - const depth = depthMap.get(t.id) || 0; - console.log( - chalk.cyan( - ` • Task ${t.id} [depth ${depth}]: ${truncate(t.title, 45)}` - ) - ); - }); - if (indirectDeps.length > 3) { - console.log( - chalk.cyan( - ` • ... and ${indirectDeps.length - 3} more indirect dependencies` - ) - ); - } - } - - // Visualize the dependency chain - if (dependencyGraphs.length > 0) { - console.log(chalk.gray(`\n Dependency chain visualization:`)); - - // Convert dependency graph to ASCII art for terminal - function visualizeDependencyGraph( - node, - prefix = '', - isLast = true, - depth = 0 - ) { - if (depth > 2) return; // Limit depth for display - - const connector = isLast ? '└── ' : '├── '; - const childPrefix = isLast ? ' ' : '│ '; - - console.log( - chalk.blue( - ` ${prefix}${connector}Task ${node.id}: ${truncate(node.title, 40)}` - ) - ); - - if (node.dependencies && node.dependencies.length > 0) { - for (let i = 0; i < node.dependencies.length; i++) { - const isLastChild = i === node.dependencies.length - 1; - visualizeDependencyGraph( - node.dependencies[i], - prefix + childPrefix, - isLastChild, - depth + 1 - ); - } - } - } - - // Visualize each dependency graph - for (const graph of dependencyGraphs) { - visualizeDependencyGraph(graph); - } - } - - console.log(); // Add spacing - } - } else { - // If no dependencies provided, use Fuse.js to find semantically related tasks - // Create fuzzy search index for all tasks - const searchOptions = { - 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 - // Search dependencies to find tasks that depend on similar things - { 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 - }; - - // Prepare task data with dependencies expanded as titles for better semantic search - const searchableTasks = data.tasks.map((task) => { - // Get titles of this task's dependencies if they exist - const dependencyTitles = - task.dependencies?.length > 0 - ? task.dependencies - .map((depId) => { - const depTask = data.tasks.find((t) => t.id === depId); - return depTask ? depTask.title : ''; - }) - .filter((title) => title) - .join(' ') - : ''; - - return { - ...task, - dependencyTitles - }; - }); - - // Create search index using Fuse.js - const fuse = new Fuse(searchableTasks, searchOptions); - - // Extract significant words and phrases from the prompt - const promptWords = prompt - .toLowerCase() - .replace(/[^\w\s-]/g, ' ') // Replace non-alphanumeric chars with spaces - .split(/\s+/) - .filter((word) => word.length > 3); // Words at least 4 chars - - // Use the user's prompt for fuzzy search - const fuzzyResults = fuse.search(prompt); - - // Also search for each significant word to catch different aspects - let wordResults = []; - for (const word of promptWords) { - if (word.length > 5) { - // Only use significant words - const results = fuse.search(word); - if (results.length > 0) { - wordResults.push(...results); - } - } - } - - // Merge and deduplicate results - const mergedResults = [...fuzzyResults]; - - // Add word results that aren't already in fuzzyResults - for (const wordResult of wordResults) { - if (!mergedResults.some((r) => r.item.id === wordResult.item.id)) { - mergedResults.push(wordResult); - } - } - - // Group search results by relevance - const highRelevance = mergedResults - .filter((result) => result.score < 0.25) - .map((result) => result.item); - - const mediumRelevance = mergedResults - .filter((result) => result.score >= 0.25 && result.score < 0.4) - .map((result) => result.item); - - // Get recent tasks (newest first) - const recentTasks = [...data.tasks] - .sort((a, b) => b.id - a.id) - .slice(0, 5); - - // Combine high relevance, medium relevance, and recent tasks - // Prioritize high relevance first - const allRelevantTasks = [...highRelevance]; - - // Add medium relevance if not already included - for (const task of mediumRelevance) { - if (!allRelevantTasks.some((t) => t.id === task.id)) { - allRelevantTasks.push(task); - } - } - - // Add recent tasks if not already included - for (const task of recentTasks) { - if (!allRelevantTasks.some((t) => t.id === task.id)) { - allRelevantTasks.push(task); - } - } - - // 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 ? '⭐ ' : ''; - 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')}`; - } - - if ( - recentTasks.length > 0 && - !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')}`; - } - - // Add detailed information about the most relevant tasks - const allDetailedTasks = [ - ...relatedTasks.slice(0, 5), - ...categoryTasks.slice(0, 2) - ]; - uniqueDetailedTasks = Array.from( - new Map(allDetailedTasks.map((t) => [t.id, t])).values() - ).slice(0, 8); - - 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`; - if (task.dependencies && task.dependencies.length > 0) { - // Format dependency list with titles - const depList = task.dependencies.map((depId) => { - const depTask = data.tasks.find((t) => t.id === depId); - return depTask - ? `Task ${depId} (${depTask.title})` - : `Task ${depId}`; - }); - 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; - contextTasks += `Implementation Details: ${truncatedDetails}\n`; - } - } - } - - // Add a concise view of the task dependency structure - 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 - const relevantTaskIds = new Set(uniqueDetailedTasks.map((t) => t.id)); - const relevantPendingTasks = data.tasks - .filter( - (t) => - (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( - (word) => - t.title.toLowerCase().includes(word) || - t.description.toLowerCase().includes(word) - )) - ) - .slice(0, 10); - - for (const task of relevantPendingTasks) { - const depsStr = - task.dependencies && task.dependencies.length > 0 - ? 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) - ) - : []; - - let commonDeps = []; // Initialize commonDeps - - if (similarPurposeTasks.length > 0) { - contextTasks += `\n\nCommon patterns for ${promptCategory ? promptCategory.label : 'similar'} tasks:`; - - // Collect dependencies from similar purpose tasks - const similarDeps = similarPurposeTasks - .filter((t) => t.dependencies && t.dependencies.length > 0) - .map((t) => t.dependencies) - .flat(); - - // Count frequency of each dependency - const depCounts = {}; - similarDeps.forEach((dep) => { - depCounts[dep] = (depCounts[dep] || 0) + 1; - }); - - // Get most common dependencies for similar tasks - commonDeps = Object.entries(depCounts) - .sort((a, b) => b[1] - a[1]) - .slice(0, 5); - - if (commonDeps.length > 0) { - contextTasks += '\nMost common dependencies for similar tasks:'; - commonDeps.forEach(([depId, count]) => { - const depTask = data.tasks.find((t) => t.id === parseInt(depId)); - if (depTask) { - contextTasks += `\n- Task ${depId} (used by ${count} similar tasks): ${depTask.title}`; - } - }); - } - } - - // Show fuzzy search analysis in CLI mode - if (outputFormat === 'text') { - console.log( - chalk.gray( - ` Fuzzy search across ${data.tasks.length} tasks using full prompt and ${promptWords.length} keywords` - ) - ); - - if (highRelevance.length > 0) { - console.log( - chalk.gray(`\n High relevance matches (score < 0.25):`) - ); - highRelevance.slice(0, 5).forEach((t) => { - console.log( - chalk.yellow(` • ⭐ Task ${t.id}: ${truncate(t.title, 50)}`) - ); - }); - } - - if (mediumRelevance.length > 0) { - console.log( - chalk.gray(`\n Medium relevance matches (score < 0.4):`) - ); - mediumRelevance.slice(0, 3).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( - chalk.gray(`\n Common dependency patterns for similar tasks:`) - ); - commonDeps.slice(0, 3).forEach(([depId, count]) => { - const depTask = data.tasks.find((t) => t.id === parseInt(depId)); - if (depTask) { - console.log( - chalk.blue( - ` • Task ${depId} (${count}x): ${truncate(depTask.title, 45)}` - ) - ); - } - }); - } - - // Add information about which tasks will be provided in detail - if (uniqueDetailedTasks.length > 0) { - console.log( - chalk.gray( - `\n Providing detailed context for ${uniqueDetailedTasks.length} most relevant tasks:` - ) - ); - uniqueDetailedTasks.forEach((t) => { - const isHighRelevance = highRelevance.some( - (ht) => ht.id === t.id - ); - const relevanceIndicator = isHighRelevance ? '⭐ ' : ''; - console.log( - chalk.cyan( - ` • ${relevanceIndicator}Task ${t.id}: ${truncate(t.title, 40)}` - ) - ); - }); - } - - console.log(); // Add spacing - } - } - - // DETERMINE THE ACTUAL COUNT OF DETAILED TASKS BEING USED FOR AI CONTEXT - let actualDetailedTasksCount = 0; - if (numericDependencies.length > 0) { - // In explicit dependency mode, we used 'uniqueDetailedTasks' derived from 'dependentTasks' - // Ensure 'uniqueDetailedTasks' from THAT scope is used or re-evaluate. - // For simplicity, let's assume 'dependentTasks' reflects the detailed tasks. - actualDetailedTasksCount = dependentTasks.length; - } else { - // In fuzzy search mode, 'uniqueDetailedTasks' from THIS scope is correct. - actualDetailedTasksCount = uniqueDetailedTasks - ? uniqueDetailedTasks.length - : 0; - } - - // Add a visual transition to show we're moving to AI generation - only for CLI - 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( - 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' - } - ) - ); - console.log(); // Add spacing - } - - // 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' + - "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'; - - // Task Structure Description (for user prompt) - const taskStructureDesc = ` + const { session, mcpLog, projectRoot, commandName, outputType } = context; + const isMCP = !!mcpLog; + + // Create a consistent logFn object regardless of context + const logFn = isMCP + ? 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), + }; + + const effectivePriority = priority || getDefaultPriority(projectRoot); + + logFn.info( + `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") => { + if (mcpLog) { + mcpLog[level](message); + } else if (outputFormat === "text") { + consoleLog(level, message); + } + }; + + /** + * Recursively builds a dependency graph for a given task + * @param {Array} tasks - All tasks from tasks.json + * @param {number} taskId - ID of the task to analyze + * @param {Set} visited - Set of already visited task IDs + * @param {Map} depthMap - Map of task ID to its depth in the graph + * @param {number} depth - Current depth in the recursion + * @return {Object} Dependency graph data + */ + function buildDependencyGraph( + tasks, + taskId, + visited = new Set(), + depthMap = new Map(), + depth = 0 + ) { + // Skip if we've already visited this task or it doesn't exist + if (visited.has(taskId)) { + return null; + } + + // Find the task + const task = tasks.find((t) => t.id === taskId); + if (!task) { + return null; + } + + // Mark as visited + visited.add(taskId); + + // Update depth if this is a deeper path to this task + if (!depthMap.has(taskId) || depth < depthMap.get(taskId)) { + depthMap.set(taskId, depth); + } + + // Process dependencies + const dependencyData = []; + if (task.dependencies && task.dependencies.length > 0) { + for (const depId of task.dependencies) { + const depData = buildDependencyGraph( + tasks, + depId, + visited, + depthMap, + depth + 1 + ); + if (depData) { + dependencyData.push(depData); + } + } + } + + return { + id: task.id, + title: task.title, + description: task.description, + status: task.status, + dependencies: dependencyData, + }; + } + + try { + // Read the existing tasks + let data = readJSON(tasksPath); + + // 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"); + // Create default tasks data structure + data = { + tasks: [], + }; + // Ensure the directory exists and write the new file + writeJSON(tasksPath, data); + report("Created new tasks.json file with empty tasks array.", "info"); + } + + // Find the highest task ID to determine the next ID + const highestId = + data.tasks.length > 0 ? Math.max(...data.tasks.map((t) => t.id)) : 0; + const newTaskId = highestId + 1; + + // Only show UI box for CLI mode + if (outputFormat === "text") { + console.log( + boxen(chalk.white.bold(`Creating New Task #${newTaskId}`), { + padding: 1, + borderColor: "blue", + borderStyle: "round", + margin: { top: 1, bottom: 1 }, + }) + ); + } + + // Validate dependencies before proceeding + const invalidDeps = dependencies.filter((depId) => { + // Ensure depId is parsed as a number for comparison + const numDepId = parseInt(depId, 10); + return isNaN(numDepId) || !data.tasks.some((t) => t.id === numDepId); + }); + + if (invalidDeps.length > 0) { + report( + `The following dependencies do not exist or are invalid: ${invalidDeps.join(", ")}`, + "warn" + ); + report("Removing invalid dependencies...", "info"); + dependencies = dependencies.filter( + (depId) => !invalidDeps.includes(depId) + ); + } + // Ensure dependencies are numbers + const numericDependencies = dependencies.map((dep) => parseInt(dep, 10)); + + // Build dependency graphs for explicitly specified dependencies + const dependencyGraphs = []; + const allRelatedTaskIds = new Set(); + const depthMap = new Map(); + + // First pass: build a complete dependency graph for each specified dependency + for (const depId of numericDependencies) { + const graph = buildDependencyGraph( + data.tasks, + depId, + new Set(), + depthMap + ); + if (graph) { + dependencyGraphs.push(graph); + } + } + + // Second pass: build a set of all related task IDs for flat analysis + for (const [taskId, depth] of depthMap.entries()) { + allRelatedTaskIds.add(taskId); + } + + let taskData; + + // Check if manual task data is provided + if (manualTaskData) { + report("Using manually provided task data", "info"); + taskData = manualTaskData; + report("DEBUG: Taking MANUAL task data path.", "debug"); + + // Basic validation for manual data + if ( + !taskData.title || + typeof taskData.title !== "string" || + !taskData.description || + typeof taskData.description !== "string" + ) { + throw new Error( + "Manual task data must include at least a title and description." + ); + } + } else { + report("DEBUG: Taking AI task generation path.", "debug"); + // --- Refactored AI Interaction --- + report(`Generating task data with AI with prompt:\n${prompt}`, "info"); + + // Create context string for task creation prompt + let contextTasks = ""; + + // Create a dependency map for better understanding of the task relationships + const taskMap = {}; + data.tasks.forEach((t) => { + // For each task, only include id, title, description, and dependencies + taskMap[t.id] = { + id: t.id, + title: t.title, + description: t.description, + dependencies: t.dependencies || [], + status: t.status, + }; + }); + + // CLI-only feedback for the dependency analysis + if (outputFormat === "text") { + console.log( + 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", + }) + ); + } + + // Initialize variables that will be used in either branch + let uniqueDetailedTasks = []; + let dependentTasks = []; + let promptCategory = null; + + if (numericDependencies.length > 0) { + // If specific dependencies were provided, focus on them + // Get all tasks that were found in the dependency graph + dependentTasks = Array.from(allRelatedTaskIds) + .map((id) => data.tasks.find((t) => t.id === id)) + .filter(Boolean); + + // Sort by depth in the dependency chain + dependentTasks.sort((a, b) => { + const depthA = depthMap.get(a.id) || 0; + const depthB = depthMap.get(b.id) || 0; + return depthA - depthB; // Lowest depth (root dependencies) first + }); + + // Limit the number of detailed tasks to avoid context explosion + uniqueDetailedTasks = dependentTasks.slice(0, 8); + + contextTasks = `\nThis task relates to a dependency structure with ${dependentTasks.length} related tasks in the chain.\n\nDirect dependencies:`; + const directDeps = data.tasks.filter((t) => + numericDependencies.includes(t.id) + ); + 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( + (t) => !numericDependencies.includes(t.id) + ); + if (indirectDeps.length > 0) { + contextTasks += `\n\nIndirect dependencies (dependencies of dependencies):`; + contextTasks += `\n${indirectDeps + .slice(0, 5) + .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) + .join("\n")}`; + if (indirectDeps.length > 5) { + contextTasks += `\n- ... and ${indirectDeps.length - 5} more indirect dependencies`; + } + } + + // Add more details about each dependency, prioritizing direct dependencies + contextTasks += `\n\nDetailed information about dependencies:`; + for (const depTask of uniqueDetailedTasks) { + const depthInfo = depthMap.get(depTask.id) + ? ` (depth: ${depthMap.get(depTask.id)})` + : ""; + const isDirect = numericDependencies.includes(depTask.id) + ? " [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`; + + // List its dependencies + if (depTask.dependencies && depTask.dependencies.length > 0) { + const depDeps = depTask.dependencies.map((dId) => { + const depDepTask = data.tasks.find((t) => t.id === dId); + return depDepTask + ? `Task ${dId}: ${depDepTask.title}` + : `Task ${dId}`; + }); + contextTasks += `Dependencies: ${depDeps.join(", ")}\n`; + } else { + contextTasks += `Dependencies: None\n`; + } + + // Add implementation details but truncate if too long + if (depTask.details) { + const truncatedDetails = + depTask.details.length > 400 + ? depTask.details.substring(0, 400) + "... (truncated)" + : depTask.details; + contextTasks += `Implementation Details: ${truncatedDetails}\n`; + } + } + + // Add dependency chain visualization + if (dependencyGraphs.length > 0) { + contextTasks += "\n\nDependency Chain Visualization:"; + + // Helper function to format dependency chain as text + function formatDependencyChain( + node, + prefix = "", + isLast = true, + depth = 0 + ) { + if (depth > 3) return ""; // Limit depth to avoid excessive nesting + + const connector = isLast ? "└── " : "├── "; + const childPrefix = isLast ? " " : "│ "; + + let result = `\n${prefix}${connector}Task ${node.id}: ${node.title}`; + + if (node.dependencies && node.dependencies.length > 0) { + for (let i = 0; i < node.dependencies.length; i++) { + const isLastChild = i === node.dependencies.length - 1; + result += formatDependencyChain( + node.dependencies[i], + prefix + childPrefix, + isLastChild, + depth + 1 + ); + } + } + + return result; + } + + // Format each dependency graph + for (const graph of dependencyGraphs) { + contextTasks += formatDependencyChain(graph); + } + } + + // Show dependency analysis in CLI mode + if (outputFormat === "text") { + if (directDeps.length > 0) { + console.log(chalk.gray(` Explicitly specified dependencies:`)); + directDeps.forEach((t) => { + console.log( + chalk.yellow(` • Task ${t.id}: ${truncate(t.title, 50)}`) + ); + }); + } + + if (indirectDeps.length > 0) { + console.log( + chalk.gray( + `\n Indirect dependencies (${indirectDeps.length} total):` + ) + ); + indirectDeps.slice(0, 3).forEach((t) => { + const depth = depthMap.get(t.id) || 0; + console.log( + chalk.cyan( + ` • Task ${t.id} [depth ${depth}]: ${truncate(t.title, 45)}` + ) + ); + }); + if (indirectDeps.length > 3) { + console.log( + chalk.cyan( + ` • ... and ${indirectDeps.length - 3} more indirect dependencies` + ) + ); + } + } + + // Visualize the dependency chain + if (dependencyGraphs.length > 0) { + console.log(chalk.gray(`\n Dependency chain visualization:`)); + + // Convert dependency graph to ASCII art for terminal + function visualizeDependencyGraph( + node, + prefix = "", + isLast = true, + depth = 0 + ) { + if (depth > 2) return; // Limit depth for display + + const connector = isLast ? "└── " : "├── "; + const childPrefix = isLast ? " " : "│ "; + + console.log( + chalk.blue( + ` ${prefix}${connector}Task ${node.id}: ${truncate(node.title, 40)}` + ) + ); + + if (node.dependencies && node.dependencies.length > 0) { + for (let i = 0; i < node.dependencies.length; i++) { + const isLastChild = i === node.dependencies.length - 1; + visualizeDependencyGraph( + node.dependencies[i], + prefix + childPrefix, + isLastChild, + depth + 1 + ); + } + } + } + + // Visualize each dependency graph + for (const graph of dependencyGraphs) { + visualizeDependencyGraph(graph); + } + } + + console.log(); // Add spacing + } + } else { + // If no dependencies provided, use Fuse.js to find semantically related tasks + // Create fuzzy search index for all tasks + const searchOptions = { + includeScore: true, // Return match scores + threshold: 0.4, // Lower threshold = stricter matching (range 0-1) + keys: [ + { 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 }, + ], + // Sort matches by score (lower is better) + shouldSort: true, + // Allow searching in nested properties + useExtendedSearch: true, + // Return up to 50 matches + limit: 50, + }; + + // Prepare task data with dependencies expanded as titles for better semantic search + const searchableTasks = data.tasks.map((task) => { + // Get titles of this task's dependencies if they exist + const dependencyTitles = + task.dependencies?.length > 0 + ? task.dependencies + .map((depId) => { + const depTask = data.tasks.find((t) => t.id === depId); + return depTask ? depTask.title : ""; + }) + .filter((title) => title) + .join(" ") + : ""; + + return { + ...task, + dependencyTitles, + }; + }); + + // Create search index using Fuse.js + const fuse = new Fuse(searchableTasks, searchOptions); + + // Extract significant words and phrases from the prompt + const promptWords = prompt + .toLowerCase() + .replace(/[^\w\s-]/g, " ") // Replace non-alphanumeric chars with spaces + .split(/\s+/) + .filter((word) => word.length > 3); // Words at least 4 chars + + // Use the user's prompt for fuzzy search + const fuzzyResults = fuse.search(prompt); + + // Also search for each significant word to catch different aspects + let wordResults = []; + for (const word of promptWords) { + if (word.length > 5) { + // Only use significant words + const results = fuse.search(word); + if (results.length > 0) { + wordResults.push(...results); + } + } + } + + // Merge and deduplicate results + const mergedResults = [...fuzzyResults]; + + // Add word results that aren't already in fuzzyResults + for (const wordResult of wordResults) { + if (!mergedResults.some((r) => r.item.id === wordResult.item.id)) { + mergedResults.push(wordResult); + } + } + + // Group search results by relevance + const highRelevance = mergedResults + .filter((result) => result.score < 0.25) + .map((result) => result.item); + + const mediumRelevance = mergedResults + .filter((result) => result.score >= 0.25 && result.score < 0.4) + .map((result) => result.item); + + // Get recent tasks (newest first) + const recentTasks = [...data.tasks] + .sort((a, b) => b.id - a.id) + .slice(0, 5); + + // Combine high relevance, medium relevance, and recent tasks + // Prioritize high relevance first + const allRelevantTasks = [...highRelevance]; + + // Add medium relevance if not already included + for (const task of mediumRelevance) { + if (!allRelevantTasks.some((t) => t.id === task.id)) { + allRelevantTasks.push(task); + } + } + + // Add recent tasks if not already included + for (const task of recentTasks) { + if (!allRelevantTasks.some((t) => t.id === task.id)) { + allRelevantTasks.push(task); + } + } + + // Get top N results for context + const relatedTasks = allRelevantTasks.slice(0, 8); + + // 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 ? "⭐ " : ""; + return `- ${relevanceMarker}Task ${t.id}: ${t.title} - ${t.description}`; + }) + .join("\n")}`; + } + + if ( + recentTasks.length > 0 && + !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")}`; + } + + // Add detailed information about the most relevant tasks + const allDetailedTasks = [...relatedTasks.slice(0, 25)]; + uniqueDetailedTasks = Array.from( + new Map(allDetailedTasks.map((t) => [t.id, t])).values() + ).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`; + if (task.dependencies && task.dependencies.length > 0) { + // Format dependency list with titles + const depList = task.dependencies.map((depId) => { + const depTask = data.tasks.find((t) => t.id === depId); + return depTask + ? `Task ${depId} (${depTask.title})` + : `Task ${depId}`; + }); + 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; + contextTasks += `Implementation Details: ${truncatedDetails}\n`; + } + } + } + + // Add a concise view of the task dependency structure + 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 + const relevantTaskIds = new Set(uniqueDetailedTasks.map((t) => t.id)); + const relevantPendingTasks = data.tasks + .filter( + (t) => + (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( + (word) => + t.title.toLowerCase().includes(word) || + t.description.toLowerCase().includes(word) + )) + ) + .slice(0, 10); + + for (const task of relevantPendingTasks) { + const depsStr = + task.dependencies && task.dependencies.length > 0 + ? task.dependencies.join(", ") + : "None"; + contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`; + } + + // Additional analysis of common patterns + 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 similar tasks:`; + + // Collect dependencies from similar purpose tasks + const similarDeps = similarPurposeTasks + .filter((t) => t.dependencies && t.dependencies.length > 0) + .map((t) => t.dependencies) + .flat(); + + // Count frequency of each dependency + const depCounts = {}; + similarDeps.forEach((dep) => { + depCounts[dep] = (depCounts[dep] || 0) + 1; + }); + + // Get most common dependencies for similar tasks + commonDeps = Object.entries(depCounts) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10); + + if (commonDeps.length > 0) { + contextTasks += "\nMost common dependencies for similar tasks:"; + commonDeps.forEach(([depId, count]) => { + const depTask = data.tasks.find((t) => t.id === parseInt(depId)); + if (depTask) { + contextTasks += `\n- Task ${depId} (used by ${count} similar tasks): ${depTask.title}`; + } + }); + } + } + + // Show fuzzy search analysis in CLI mode + if (outputFormat === "text") { + console.log( + chalk.gray( + ` Context search across ${data.tasks.length} tasks using full prompt and ${promptWords.length} keywords` + ) + ); + + if (highRelevance.length > 0) { + console.log( + chalk.gray(`\n High relevance matches (score < 0.25):`) + ); + highRelevance.slice(0, 25).forEach((t) => { + console.log( + chalk.yellow(` • ⭐ Task ${t.id}: ${truncate(t.title, 50)}`) + ); + }); + } + + if (mediumRelevance.length > 0) { + console.log( + chalk.gray(`\n Medium relevance matches (score < 0.4):`) + ); + mediumRelevance.slice(0, 10).forEach((t) => { + console.log( + chalk.green(` • Task ${t.id}: ${truncate(t.title, 50)}`) + ); + }); + } + + // Show dependency patterns + if (commonDeps && commonDeps.length > 0) { + console.log( + chalk.gray(`\n Common dependency patterns for similar tasks:`) + ); + commonDeps.slice(0, 3).forEach(([depId, count]) => { + const depTask = data.tasks.find((t) => t.id === parseInt(depId)); + if (depTask) { + console.log( + chalk.blue( + ` • Task ${depId} (${count}x): ${truncate(depTask.title, 45)}` + ) + ); + } + }); + } + + // Add information about which tasks will be provided in detail + if (uniqueDetailedTasks.length > 0) { + console.log( + chalk.gray( + `\n Providing detailed context for ${uniqueDetailedTasks.length} most relevant tasks:` + ) + ); + uniqueDetailedTasks.forEach((t) => { + const isHighRelevance = highRelevance.some( + (ht) => ht.id === t.id + ); + const relevanceIndicator = isHighRelevance ? "⭐ " : ""; + console.log( + chalk.cyan( + ` • ${relevanceIndicator}Task ${t.id}: ${truncate(t.title, 40)}` + ) + ); + }); + } + + console.log(); // Add spacing + } + } + + // DETERMINE THE ACTUAL COUNT OF DETAILED TASKS BEING USED FOR AI CONTEXT + let actualDetailedTasksCount = 0; + if (numericDependencies.length > 0) { + // In explicit dependency mode, we used 'uniqueDetailedTasks' derived from 'dependentTasks' + // Ensure 'uniqueDetailedTasks' from THAT scope is used or re-evaluate. + // For simplicity, let's assume 'dependentTasks' reflects the detailed tasks. + actualDetailedTasksCount = dependentTasks.length; + } else { + // In fuzzy search mode, 'uniqueDetailedTasks' from THIS scope is correct. + actualDetailedTasksCount = uniqueDetailedTasks + ? uniqueDetailedTasks.length + : 0; + } + + // Add a visual transition to show we're moving to AI generation - only for CLI + 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( + numericDependencies.length > 0 + ? dependentTasks.length // Use length of tasks from explicit dependency path + : uniqueDetailedTasks.length // Use length of tasks from fuzzy search path + )}`, + { + padding: { top: 0, bottom: 1, left: 1, right: 1 }, + margin: { top: 1, bottom: 0 }, + borderColor: "white", + borderStyle: "round", + } + ) + ); + console.log(); // Add spacing + } + + // 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" + + "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"; + + // Task Structure Description (for user prompt) + const taskStructureDesc = ` { "title": "Task title goes here", "description": "A concise one or two sentence description of what the task involves", @@ -903,22 +852,22 @@ async function addTask( } `; - // Add any manually provided details to the prompt for context - let contextFromArgs = ''; - if (manualTaskData?.title) - contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`; - if (manualTaskData?.description) - contextFromArgs += `\n- Suggested Description: "${manualTaskData.description}"`; - if (manualTaskData?.details) - contextFromArgs += `\n- Additional Details Context: "${manualTaskData.details}"`; - if (manualTaskData?.testStrategy) - contextFromArgs += `\n- Additional Test Strategy Context: "${manualTaskData.testStrategy}"`; + // Add any manually provided details to the prompt for context + let contextFromArgs = ""; + if (manualTaskData?.title) + contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`; + if (manualTaskData?.description) + contextFromArgs += `\n- Suggested Description: "${manualTaskData.description}"`; + if (manualTaskData?.details) + contextFromArgs += `\n- Additional Details Context: "${manualTaskData.details}"`; + if (manualTaskData?.testStrategy) + contextFromArgs += `\n- Additional Test Strategy Context: "${manualTaskData.testStrategy}"`; - // User Prompt - 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. + // User Prompt + 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. @@ -928,281 +877,297 @@ async function addTask( Make sure the details and test strategy are comprehensive and specific. DO NOT include the task ID in the title. `; - // Start the loading indicator - only for text mode - if (outputFormat === 'text') { - loadingIndicator = startLoadingIndicator( - `Generating new task with ${useResearch ? 'Research' : 'Main'} AI...\n` - ); - } + // Start the loading indicator - only for text mode + if (outputFormat === "text") { + loadingIndicator = startLoadingIndicator( + `Generating new task with ${useResearch ? "Research" : "Main"} AI... \n` + ); + } - try { - const serviceRole = useResearch ? 'research' : 'main'; - report('DEBUG: Calling generateObjectService...', 'debug'); + try { + const serviceRole = useResearch ? "research" : "main"; + report("DEBUG: Calling generateObjectService...", "debug"); - aiServiceResponse = await generateObjectService({ - // Capture the full response - role: serviceRole, - session: session, - projectRoot: projectRoot, - schema: AiTaskDataSchema, - 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 - }); - report('DEBUG: generateObjectService returned successfully.', 'debug'); + aiServiceResponse = await generateObjectService({ + // Capture the full response + role: serviceRole, + session: session, + projectRoot: projectRoot, + schema: AiTaskDataSchema, + 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 + }); + report("DEBUG: generateObjectService returned successfully.", "debug"); - if (!aiServiceResponse || !aiServiceResponse.mainResult) { - throw new Error( - 'AI service did not return the expected object structure.' - ); - } + if (!aiServiceResponse || !aiServiceResponse.mainResult) { + throw new Error( + "AI service did not return the expected object structure." + ); + } - // Prefer mainResult if it looks like a valid task object, otherwise try mainResult.object - if ( - aiServiceResponse.mainResult.title && - aiServiceResponse.mainResult.description - ) { - taskData = aiServiceResponse.mainResult; - } else if ( - aiServiceResponse.mainResult.object && - aiServiceResponse.mainResult.object.title && - aiServiceResponse.mainResult.object.description - ) { - taskData = aiServiceResponse.mainResult.object; - } else { - throw new Error('AI service did not return a valid task object.'); - } + // Prefer mainResult if it looks like a valid task object, otherwise try mainResult.object + if ( + aiServiceResponse.mainResult.title && + aiServiceResponse.mainResult.description + ) { + taskData = aiServiceResponse.mainResult; + } else if ( + aiServiceResponse.mainResult.object && + aiServiceResponse.mainResult.object.title && + aiServiceResponse.mainResult.object.description + ) { + taskData = aiServiceResponse.mainResult.object; + } else { + throw new Error("AI service did not return a valid task object."); + } - report('Successfully generated task data from AI.', 'success'); - } catch (error) { - report( - `DEBUG: generateObjectService caught error: ${error.message}`, - 'debug' - ); - report(`Error generating task with AI: ${error.message}`, 'error'); - if (loadingIndicator) stopLoadingIndicator(loadingIndicator); - throw error; // Re-throw error after logging - } finally { - report('DEBUG: generateObjectService finally block reached.', 'debug'); - if (loadingIndicator) stopLoadingIndicator(loadingIndicator); // Ensure indicator stops - } - // --- End Refactored AI Interaction --- - } + report("Successfully generated task data from AI.", "success"); - // Create the new task object - const newTask = { - id: newTaskId, - title: taskData.title, - description: taskData.description, - 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 - }; + // 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" + ); + report(`Error generating task with AI: ${error.message}`, "error"); + throw error; // Re-throw error after logging + } finally { + report("DEBUG: generateObjectService finally block reached.", "debug"); + // Clean up if somehow still running + if (loadingIndicator) { + stopLoadingIndicator(loadingIndicator); + } + } + // --- End Refactored AI Interaction --- + } - // Additional check: validate all dependencies in the AI response - if (taskData.dependencies?.length) { - const allValidDeps = taskData.dependencies.every((depId) => { - const numDepId = parseInt(depId, 10); - return !isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId); - }); + // Create the new task object + const newTask = { + id: newTaskId, + title: taskData.title, + description: taskData.description, + 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 + }; - if (!allValidDeps) { - report( - 'AI suggested invalid dependencies. Filtering them out...', - 'warn' - ); - newTask.dependencies = taskData.dependencies.filter((depId) => { - const numDepId = parseInt(depId, 10); - return !isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId); - }); - } - } + // Additional check: validate all dependencies in the AI response + if (taskData.dependencies?.length) { + const allValidDeps = taskData.dependencies.every((depId) => { + const numDepId = parseInt(depId, 10); + return !isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId); + }); - // Add the task to the tasks array - data.tasks.push(newTask); + if (!allValidDeps) { + report( + "AI suggested invalid dependencies. Filtering them out...", + "warn" + ); + newTask.dependencies = taskData.dependencies.filter((depId) => { + const numDepId = parseInt(depId, 10); + return !isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId); + }); + } + } - report('DEBUG: Writing tasks.json...', 'debug'); - // Write the updated tasks to the file - writeJSON(tasksPath, data); - report('DEBUG: tasks.json written.', 'debug'); + // Add the task to the tasks array + data.tasks.push(newTask); - // Generate markdown task files - 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: Writing tasks.json...", "debug"); + // Write the updated tasks to the file + writeJSON(tasksPath, data); + report("DEBUG: tasks.json written.", "debug"); - // Show success message - only for text output (CLI) - if (outputFormat === 'text') { - const table = new Table({ - head: [ - chalk.cyan.bold('ID'), - chalk.cyan.bold('Title'), - chalk.cyan.bold('Description') - ], - colWidths: [5, 30, 50] // Adjust widths as needed - }); + // Generate markdown task files + 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"); - table.push([ - newTask.id, - truncate(newTask.title, 27), - truncate(newTask.description, 47) - ]); + // Show success message - only for text output (CLI) + if (outputFormat === "text") { + const table = new Table({ + head: [ + chalk.cyan.bold("ID"), + chalk.cyan.bold("Title"), + chalk.cyan.bold("Description"), + ], + colWidths: [5, 30, 50], // Adjust widths as needed + }); - console.log(chalk.green('✅ New task created successfully:')); - console.log(table.toString()); + table.push([ + newTask.id, + truncate(newTask.title, 27), + truncate(newTask.description, 47), + ]); - // Helper to get priority color - const getPriorityColor = (p) => { - switch (p?.toLowerCase()) { - case 'high': - return 'red'; - case 'low': - return 'gray'; - case 'medium': - default: - return 'yellow'; - } - }; + console.log(chalk.green("✓ New task created successfully:")); + console.log(table.toString()); - // Check if AI added new dependencies that weren't explicitly provided - const aiAddedDeps = newTask.dependencies.filter( - (dep) => !numericDependencies.includes(dep) - ); + // Helper to get priority color + const getPriorityColor = (p) => { + switch (p?.toLowerCase()) { + case "high": + return "red"; + case "low": + return "gray"; + case "medium": + default: + return "yellow"; + } + }; - // Check if AI removed any dependencies that were explicitly provided - const aiRemovedDeps = numericDependencies.filter( - (dep) => !newTask.dependencies.includes(dep) - ); + // Check if AI added new dependencies that weren't explicitly provided + const aiAddedDeps = newTask.dependencies.filter( + (dep) => !numericDependencies.includes(dep) + ); - // Get task titles for dependencies to display - const depTitles = {}; - newTask.dependencies.forEach((dep) => { - const depTask = data.tasks.find((t) => t.id === dep); - if (depTask) { - depTitles[dep] = truncate(depTask.title, 30); - } - }); + // Check if AI removed any dependencies that were explicitly provided + const aiRemovedDeps = numericDependencies.filter( + (dep) => !newTask.dependencies.includes(dep) + ); - // Prepare dependency display string - let dependencyDisplay = ''; - if (newTask.dependencies.length > 0) { - dependencyDisplay = chalk.white('Dependencies:') + '\n'; - newTask.dependencies.forEach((dep) => { - const isAiAdded = aiAddedDeps.includes(dep); - const depType = isAiAdded ? chalk.yellow(' (AI suggested)') : ''; - dependencyDisplay += - chalk.white( - ` - ${dep}: ${depTitles[dep] || 'Unknown task'}${depType}` - ) + '\n'; - }); - } else { - dependencyDisplay = chalk.white('Dependencies: None') + '\n'; - } + // Get task titles for dependencies to display + const depTitles = {}; + newTask.dependencies.forEach((dep) => { + const depTask = data.tasks.find((t) => t.id === dep); + if (depTask) { + depTitles[dep] = truncate(depTask.title, 30); + } + }); - // Add info about removed dependencies if any - if (aiRemovedDeps.length > 0) { - dependencyDisplay += - 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'; - }); - } + // Prepare dependency display string + let dependencyDisplay = ""; + if (newTask.dependencies.length > 0) { + dependencyDisplay = chalk.white("Dependencies:") + "\n"; + newTask.dependencies.forEach((dep) => { + const isAiAdded = aiAddedDeps.includes(dep); + const depType = isAiAdded ? chalk.yellow(" (AI suggested)") : ""; + dependencyDisplay += + chalk.white( + ` - ${dep}: ${depTitles[dep] || "Unknown task"}${depType}` + ) + "\n"; + }); + } else { + dependencyDisplay = chalk.white("Dependencies: None") + "\n"; + } - // Add dependency analysis summary - let dependencyAnalysis = ''; - if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) { - dependencyAnalysis = - '\n' + chalk.white.bold('Dependency Analysis:') + '\n'; - if (aiAddedDeps.length > 0) { - dependencyAnalysis += - chalk.green( - `AI identified ${aiAddedDeps.length} additional dependencies` - ) + '\n'; - } - if (aiRemovedDeps.length > 0) { - dependencyAnalysis += - chalk.yellow( - `AI excluded ${aiRemovedDeps.length} user-provided dependencies` - ) + '\n'; - } - } + // Add info about removed dependencies if any + if (aiRemovedDeps.length > 0) { + dependencyDisplay += + 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"; + }); + } - // Show success message box - console.log( - boxen( - chalk.white.bold(`Task ${newTaskId} Created Successfully`) + - '\n\n' + - chalk.white(`Title: ${newTask.title}`) + - '\n' + - chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) + - '\n' + - chalk.white( - `Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}` - ) + - '\n\n' + - dependencyDisplay + - dependencyAnalysis + - '\n' + - chalk.white.bold('Next Steps:') + - '\n' + - chalk.cyan( - `1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details` - ) + - '\n' + - chalk.cyan( - `2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it` - ) + - '\n' + - chalk.cyan( - `3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks` - ), - { padding: 1, borderColor: 'green', borderStyle: 'round' } - ) - ); + // Add dependency analysis summary + let dependencyAnalysis = ""; + if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) { + dependencyAnalysis = + "\n" + chalk.white.bold("Dependency Analysis:") + "\n"; + if (aiAddedDeps.length > 0) { + dependencyAnalysis += + chalk.green( + `AI identified ${aiAddedDeps.length} additional dependencies` + ) + "\n"; + } + if (aiRemovedDeps.length > 0) { + dependencyAnalysis += + chalk.yellow( + `AI excluded ${aiRemovedDeps.length} user-provided dependencies` + ) + "\n"; + } + } - // Display AI Usage Summary if telemetryData is available - if ( - aiServiceResponse && - aiServiceResponse.telemetryData && - (outputType === 'cli' || outputType === 'text') - ) { - displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli'); - } - } + // Show success message box + console.log( + boxen( + chalk.white.bold(`Task ${newTaskId} Created Successfully`) + + "\n\n" + + chalk.white(`Title: ${newTask.title}`) + + "\n" + + chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) + + "\n" + + chalk.white( + `Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}` + ) + + "\n\n" + + dependencyDisplay + + dependencyAnalysis + + "\n" + + chalk.white.bold("Next Steps:") + + "\n" + + chalk.cyan( + `1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details` + ) + + "\n" + + chalk.cyan( + `2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it` + ) + + "\n" + + chalk.cyan( + `3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks` + ), + { padding: 1, borderColor: "green", borderStyle: "round" } + ) + ); - report( - `DEBUG: Returning new task ID: ${newTaskId} and telemetry.`, - 'debug' - ); - return { - newTaskId: newTaskId, - telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null - }; - } catch (error) { - // Stop any loading indicator on error - if (loadingIndicator) { - stopLoadingIndicator(loadingIndicator); - } + // Display AI Usage Summary if telemetryData is available + if ( + aiServiceResponse && + aiServiceResponse.telemetryData && + (outputType === "cli" || outputType === "text") + ) { + displayAiUsageSummary(aiServiceResponse.telemetryData, "cli"); + } + } - 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 - throw error; - } + report( + `DEBUG: Returning new task ID: ${newTaskId} and telemetry.`, + "debug" + ); + return { + newTaskId: newTaskId, + telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null, + }; + } catch (error) { + // Stop any loading indicator on error + if (loadingIndicator) { + stopLoadingIndicator(loadingIndicator); + } + + 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 + throw error; + } } export default addTask; diff --git a/scripts/modules/task-manager/clear-subtasks.js b/scripts/modules/task-manager/clear-subtasks.js index 9ce01a27..f07dd897 100644 --- a/scripts/modules/task-manager/clear-subtasks.js +++ b/scripts/modules/task-manager/clear-subtasks.js @@ -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,140 +13,138 @@ 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}...`); + const data = readJSON(tasksPath); + if (!data || !data.tasks) { + log("error", "No valid tasks found."); + process.exit(1); + } - log('info', `Reading tasks from ${tasksPath}...`); - const data = readJSON(tasksPath); - if (!data || !data.tasks) { - log('error', 'No valid tasks found.'); - process.exit(1); - } + if (!isSilentMode()) { + console.log( + boxen(chalk.white.bold("Clearing Subtasks"), { + padding: 1, + borderColor: "blue", + borderStyle: "round", + margin: { top: 1, bottom: 1 }, + }) + ); + } - if (!isSilentMode()) { - console.log( - boxen(chalk.white.bold('Clearing Subtasks'), { - padding: 1, - borderColor: 'blue', - borderStyle: 'round', - margin: { top: 1, bottom: 1 } - }) - ); - } + // Handle multiple task IDs (comma-separated) + const taskIdArray = taskIds.split(",").map((id) => id.trim()); + let clearedCount = 0; - // Handle multiple task IDs (comma-separated) - 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"), + ], + colWidths: [10, 50, 20], + style: { head: [], border: [] }, + }); - // 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') - ], - colWidths: [10, 50, 20], - style: { head: [], border: [] } - }); + taskIdArray.forEach((taskId) => { + const id = parseInt(taskId, 10); + if (isNaN(id)) { + log("error", `Invalid task ID: ${taskId}`); + return; + } - taskIdArray.forEach((taskId) => { - const id = parseInt(taskId, 10); - if (isNaN(id)) { - log('error', `Invalid task ID: ${taskId}`); - return; - } + const task = data.tasks.find((t) => t.id === id); + if (!task) { + log("error", `Task ${id} not found`); + return; + } - const task = data.tasks.find((t) => t.id === id); - if (!task) { - log('error', `Task ${id} not found`); - return; - } + if (!task.subtasks || task.subtasks.length === 0) { + log("info", `Task ${id} has no subtasks to clear`); + summaryTable.push([ + id.toString(), + truncate(task.title, 47), + chalk.yellow("No subtasks"), + ]); + return; + } - if (!task.subtasks || task.subtasks.length === 0) { - log('info', `Task ${id} has no subtasks to clear`); - summaryTable.push([ - id.toString(), - truncate(task.title, 47), - chalk.yellow('No subtasks') - ]); - return; - } + const subtaskCount = task.subtasks.length; + task.subtasks = []; + clearedCount++; + log("info", `Cleared ${subtaskCount} subtasks from task ${id}`); - const subtaskCount = task.subtasks.length; - task.subtasks = []; - clearedCount++; - log('info', `Cleared ${subtaskCount} subtasks from task ${id}`); + summaryTable.push([ + id.toString(), + truncate(task.title, 47), + chalk.green(`${subtaskCount} subtasks cleared`), + ]); + }); - summaryTable.push([ - id.toString(), - truncate(task.title, 47), - chalk.green(`${subtaskCount} subtasks cleared`) - ]); - }); + if (clearedCount > 0) { + writeJSON(tasksPath, data); - if (clearedCount > 0) { - writeJSON(tasksPath, data); + // Show summary table + if (!isSilentMode()) { + console.log( + 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", + }) + ); + console.log(summaryTable.toString()); + } - // Show summary table - if (!isSilentMode()) { - console.log( - 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' - }) - ); - console.log(summaryTable.toString()); - } + // Regenerate task files to reflect changes + log("info", "Regenerating task files..."); + generateTaskFiles(tasksPath, path.dirname(tasksPath)); - // Regenerate task files to reflect changes - log('info', 'Regenerating task files...'); - generateTaskFiles(tasksPath, path.dirname(tasksPath)); + // Success message + if (!isSilentMode()) { + console.log( + boxen( + chalk.green( + `Successfully cleared subtasks from ${chalk.bold(clearedCount)} task(s)` + ), + { + padding: 1, + borderColor: "green", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); - // Success message - if (!isSilentMode()) { - console.log( - boxen( - chalk.green( - `Successfully cleared subtasks from ${chalk.bold(clearedCount)} task(s)` - ), - { - padding: 1, - borderColor: 'green', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); - - // Next steps suggestion - console.log( - boxen( - chalk.white.bold('Next Steps:') + - '\n\n' + - `${chalk.cyan('1.')} Run ${chalk.yellow('task-master expand --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 } - } - ) - ); - } - } else { - if (!isSilentMode()) { - console.log( - boxen(chalk.yellow('No subtasks were cleared'), { - padding: 1, - borderColor: 'yellow', - borderStyle: 'round', - margin: { top: 1 } - }) - ); - } - } + // Next steps suggestion + console.log( + boxen( + chalk.white.bold("Next Steps:") + + "\n\n" + + `${chalk.cyan("1.")} Run ${chalk.yellow("task-master expand --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 }, + } + ) + ); + } + } else { + if (!isSilentMode()) { + console.log( + boxen(chalk.yellow("No subtasks were cleared"), { + padding: 1, + borderColor: "yellow", + borderStyle: "round", + margin: { top: 1 }, + }) + ); + } + } } export default clearSubtasks; diff --git a/scripts/modules/task-manager/list-tasks.js b/scripts/modules/task-manager/list-tasks.js index 1aea9fee..d82768cd 100644 --- a/scripts/modules/task-manager/list-tasks.js +++ b/scripts/modules/task-manager/list-tasks.js @@ -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'; + log, + readJSON, + truncate, + readComplexityReport, + addComplexityToTask, +} from "../utils.js"; +import findNextTask from "./find-next-task.js"; import { - displayBanner, - getStatusWithColor, - formatDependenciesWithStatus, - getComplexityWithColor, - createProgressBar -} from '../ui.js'; + displayBanner, + getStatusWithColor, + formatDependenciesWithStatus, + getComplexityWithColor, + createProgressBar, +} from "../ui.js"; /** * List all tasks @@ -29,739 +29,734 @@ import { * @returns {Object} - Task list result for json format */ function listTasks( - tasksPath, - statusFilter, - reportPath = null, - withSubtasks = false, - outputFormat = 'text' + tasksPath, + statusFilter, + reportPath = null, + withSubtasks = false, + outputFormat = "text" ) { - try { - // Only display banner for text output - if (outputFormat === 'text') { - displayBanner(); - } + try { + const data = readJSON(tasksPath); // Reads the whole tasks.json + if (!data || !data.tasks) { + throw new Error(`No valid tasks found in ${tasksPath}`); + } - const data = readJSON(tasksPath); // Reads the whole tasks.json - if (!data || !data.tasks) { - throw new Error(`No valid tasks found in ${tasksPath}`); - } + // Add complexity scores to tasks if report exists + const complexityReport = readComplexityReport(reportPath); + // Apply complexity scores to tasks + if (complexityReport && complexityReport.complexityAnalysis) { + data.tasks.forEach((task) => addComplexityToTask(task, complexityReport)); + } - // Add complexity scores to tasks if report exists - const complexityReport = readComplexityReport(reportPath); - // Apply complexity scores to tasks - if (complexityReport && complexityReport.complexityAnalysis) { - data.tasks.forEach((task) => addComplexityToTask(task, complexityReport)); - } + // Filter tasks by status if specified + const filteredTasks = + statusFilter && statusFilter.toLowerCase() !== "all" // <-- Added check for 'all' + ? data.tasks.filter( + (task) => + task.status && + task.status.toLowerCase() === statusFilter.toLowerCase() + ) + : data.tasks; // Default to all tasks if no filter or filter is 'all' - // Filter tasks by status if specified - const filteredTasks = - statusFilter && statusFilter.toLowerCase() !== 'all' // <-- Added check for 'all' - ? data.tasks.filter( - (task) => - task.status && - task.status.toLowerCase() === statusFilter.toLowerCase() - ) - : data.tasks; // Default to all tasks if no filter or filter is 'all' + // Calculate completion statistics + const totalTasks = data.tasks.length; + const completedTasks = data.tasks.filter( + (task) => task.status === "done" || task.status === "completed" + ).length; + const completionPercentage = + totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0; - // Calculate completion statistics - const totalTasks = data.tasks.length; - const completedTasks = data.tasks.filter( - (task) => task.status === 'done' || task.status === 'completed' - ).length; - const completionPercentage = - totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0; + // Count statuses for tasks + const doneCount = completedTasks; + const inProgressCount = data.tasks.filter( + (task) => task.status === "in-progress" + ).length; + const pendingCount = data.tasks.filter( + (task) => task.status === "pending" + ).length; + const blockedCount = data.tasks.filter( + (task) => task.status === "blocked" + ).length; + const deferredCount = data.tasks.filter( + (task) => task.status === "deferred" + ).length; + const cancelledCount = data.tasks.filter( + (task) => task.status === "cancelled" + ).length; - // Count statuses for tasks - const doneCount = completedTasks; - const inProgressCount = data.tasks.filter( - (task) => task.status === 'in-progress' - ).length; - const pendingCount = data.tasks.filter( - (task) => task.status === 'pending' - ).length; - const blockedCount = data.tasks.filter( - (task) => task.status === 'blocked' - ).length; - const deferredCount = data.tasks.filter( - (task) => task.status === 'deferred' - ).length; - const cancelledCount = data.tasks.filter( - (task) => task.status === 'cancelled' - ).length; + // Count subtasks and their statuses + let totalSubtasks = 0; + let completedSubtasks = 0; + let inProgressSubtasks = 0; + let pendingSubtasks = 0; + let blockedSubtasks = 0; + let deferredSubtasks = 0; + let cancelledSubtasks = 0; - // Count subtasks and their statuses - let totalSubtasks = 0; - let completedSubtasks = 0; - let inProgressSubtasks = 0; - let pendingSubtasks = 0; - let blockedSubtasks = 0; - let deferredSubtasks = 0; - let cancelledSubtasks = 0; + data.tasks.forEach((task) => { + if (task.subtasks && task.subtasks.length > 0) { + totalSubtasks += task.subtasks.length; + completedSubtasks += task.subtasks.filter( + (st) => st.status === "done" || st.status === "completed" + ).length; + inProgressSubtasks += task.subtasks.filter( + (st) => st.status === "in-progress" + ).length; + pendingSubtasks += task.subtasks.filter( + (st) => st.status === "pending" + ).length; + blockedSubtasks += task.subtasks.filter( + (st) => st.status === "blocked" + ).length; + deferredSubtasks += task.subtasks.filter( + (st) => st.status === "deferred" + ).length; + cancelledSubtasks += task.subtasks.filter( + (st) => st.status === "cancelled" + ).length; + } + }); - data.tasks.forEach((task) => { - if (task.subtasks && task.subtasks.length > 0) { - totalSubtasks += task.subtasks.length; - completedSubtasks += task.subtasks.filter( - (st) => st.status === 'done' || st.status === 'completed' - ).length; - inProgressSubtasks += task.subtasks.filter( - (st) => st.status === 'in-progress' - ).length; - pendingSubtasks += task.subtasks.filter( - (st) => st.status === 'pending' - ).length; - blockedSubtasks += task.subtasks.filter( - (st) => st.status === 'blocked' - ).length; - deferredSubtasks += task.subtasks.filter( - (st) => st.status === 'deferred' - ).length; - cancelledSubtasks += task.subtasks.filter( - (st) => st.status === 'cancelled' - ).length; - } - }); + const subtaskCompletionPercentage = + totalSubtasks > 0 ? (completedSubtasks / totalSubtasks) * 100 : 0; - const subtaskCompletionPercentage = - totalSubtasks > 0 ? (completedSubtasks / totalSubtasks) * 100 : 0; + // For JSON output, return structured data + if (outputFormat === "json") { + // *** Modification: Remove 'details' field for JSON output *** + const tasksWithoutDetails = filteredTasks.map((task) => { + // <-- USES filteredTasks! + // Omit 'details' from the parent task + const { details, ...taskRest } = task; - // For JSON output, return structured data - if (outputFormat === 'json') { - // *** Modification: Remove 'details' field for JSON output *** - const tasksWithoutDetails = filteredTasks.map((task) => { - // <-- USES filteredTasks! - // Omit 'details' from the parent task - const { details, ...taskRest } = task; + // If subtasks exist, omit 'details' from them too + if (taskRest.subtasks && Array.isArray(taskRest.subtasks)) { + taskRest.subtasks = taskRest.subtasks.map((subtask) => { + const { details: subtaskDetails, ...subtaskRest } = subtask; + return subtaskRest; + }); + } + return taskRest; + }); + // *** End of Modification *** - // If subtasks exist, omit 'details' from them too - if (taskRest.subtasks && Array.isArray(taskRest.subtasks)) { - taskRest.subtasks = taskRest.subtasks.map((subtask) => { - const { details: subtaskDetails, ...subtaskRest } = subtask; - return subtaskRest; - }); - } - return taskRest; - }); - // *** End of Modification *** + return { + tasks: tasksWithoutDetails, // <--- THIS IS THE ARRAY BEING RETURNED + filter: statusFilter || "all", // Return the actual filter used + stats: { + total: totalTasks, + completed: doneCount, + inProgress: inProgressCount, + pending: pendingCount, + blocked: blockedCount, + deferred: deferredCount, + cancelled: cancelledCount, + completionPercentage, + subtasks: { + total: totalSubtasks, + completed: completedSubtasks, + inProgress: inProgressSubtasks, + pending: pendingSubtasks, + blocked: blockedSubtasks, + deferred: deferredSubtasks, + cancelled: cancelledSubtasks, + completionPercentage: subtaskCompletionPercentage, + }, + }, + }; + } - return { - tasks: tasksWithoutDetails, // <--- THIS IS THE ARRAY BEING RETURNED - filter: statusFilter || 'all', // Return the actual filter used - stats: { - total: totalTasks, - completed: doneCount, - inProgress: inProgressCount, - pending: pendingCount, - blocked: blockedCount, - deferred: deferredCount, - cancelled: cancelledCount, - completionPercentage, - subtasks: { - total: totalSubtasks, - completed: completedSubtasks, - inProgress: inProgressSubtasks, - pending: pendingSubtasks, - blocked: blockedSubtasks, - deferred: deferredSubtasks, - cancelled: cancelledSubtasks, - completionPercentage: subtaskCompletionPercentage - } - } - }; - } + // ... existing code for text output ... - // ... existing code for text output ... + // Calculate status breakdowns as percentages of total + const taskStatusBreakdown = { + "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, + }; - // Calculate status breakdowns as percentages of total - const taskStatusBreakdown = { - '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 - }; + const subtaskStatusBreakdown = { + "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, + }; - const subtaskStatusBreakdown = { - '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 - }; + // Create progress bars with status breakdowns + const taskProgressBar = createProgressBar( + completionPercentage, + 30, + taskStatusBreakdown + ); + const subtaskProgressBar = createProgressBar( + subtaskCompletionPercentage, + 30, + subtaskStatusBreakdown + ); - // Create progress bars with status breakdowns - const taskProgressBar = createProgressBar( - completionPercentage, - 30, - taskStatusBreakdown - ); - const subtaskProgressBar = createProgressBar( - subtaskCompletionPercentage, - 30, - subtaskStatusBreakdown - ); + // Calculate dependency statistics + const completedTaskIds = new Set( + data.tasks + .filter((t) => t.status === "done" || t.status === "completed") + .map((t) => t.id) + ); - // Calculate dependency statistics - const completedTaskIds = new Set( - data.tasks - .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.dependencies || t.dependencies.length === 0) + ).length; - const tasksWithNoDeps = data.tasks.filter( - (t) => - 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.dependencies && + t.dependencies.length > 0 && + t.dependencies.every((depId) => completedTaskIds.has(depId)) + ).length; - const tasksWithAllDepsSatisfied = data.tasks.filter( - (t) => - t.status !== 'done' && - t.status !== 'completed' && - t.dependencies && - t.dependencies.length > 0 && - t.dependencies.every((depId) => completedTaskIds.has(depId)) - ).length; + const tasksWithUnsatisfiedDeps = data.tasks.filter( + (t) => + t.status !== "done" && + t.status !== "completed" && + t.dependencies && + t.dependencies.length > 0 && + !t.dependencies.every((depId) => completedTaskIds.has(depId)) + ).length; - const tasksWithUnsatisfiedDeps = data.tasks.filter( - (t) => - t.status !== 'done' && - t.status !== 'completed' && - t.dependencies && - t.dependencies.length > 0 && - !t.dependencies.every((depId) => completedTaskIds.has(depId)) - ).length; + // Calculate total tasks ready to work on (no deps + satisfied deps) + const tasksReadyToWork = tasksWithNoDeps + tasksWithAllDepsSatisfied; - // Calculate total tasks ready to work on (no deps + satisfied deps) - const tasksReadyToWork = tasksWithNoDeps + tasksWithAllDepsSatisfied; + // Calculate most depended-on tasks + const dependencyCount = {}; + data.tasks.forEach((task) => { + if (task.dependencies && task.dependencies.length > 0) { + task.dependencies.forEach((depId) => { + dependencyCount[depId] = (dependencyCount[depId] || 0) + 1; + }); + } + }); - // Calculate most depended-on tasks - const dependencyCount = {}; - data.tasks.forEach((task) => { - if (task.dependencies && task.dependencies.length > 0) { - task.dependencies.forEach((depId) => { - dependencyCount[depId] = (dependencyCount[depId] || 0) + 1; - }); - } - }); + // Find the most depended-on task + let mostDependedOnTaskId = null; + let maxDependents = 0; - // Find the most depended-on task - let mostDependedOnTaskId = null; - let maxDependents = 0; + for (const [taskId, count] of Object.entries(dependencyCount)) { + if (count > maxDependents) { + maxDependents = count; + mostDependedOnTaskId = parseInt(taskId); + } + } - for (const [taskId, count] of Object.entries(dependencyCount)) { - if (count > maxDependents) { - maxDependents = count; - mostDependedOnTaskId = parseInt(taskId); - } - } + // Get the most depended-on task + const mostDependedOnTask = + mostDependedOnTaskId !== null + ? data.tasks.find((t) => t.id === mostDependedOnTaskId) + : null; - // Get the most depended-on task - const mostDependedOnTask = - mostDependedOnTaskId !== null - ? data.tasks.find((t) => t.id === mostDependedOnTaskId) - : null; + // Calculate average dependencies per task + const totalDependencies = data.tasks.reduce( + (sum, task) => sum + (task.dependencies ? task.dependencies.length : 0), + 0 + ); + const avgDependenciesPerTask = totalDependencies / data.tasks.length; - // Calculate average dependencies per task - const totalDependencies = data.tasks.reduce( - (sum, task) => sum + (task.dependencies ? task.dependencies.length : 0), - 0 - ); - const avgDependenciesPerTask = totalDependencies / data.tasks.length; + // Find next task to work on, passing the complexity report + const nextItem = findNextTask(data.tasks, complexityReport); - // Find next task to work on, passing the complexity report - const nextItem = findNextTask(data.tasks, complexityReport); + // Get terminal width - more reliable method + let terminalWidth; + try { + // Try to get the actual terminal columns + terminalWidth = process.stdout.columns; + } catch (e) { + // Fallback if columns cannot be determined + log("debug", "Could not determine terminal width, using default"); + } + // Ensure we have a reasonable default if detection fails + terminalWidth = terminalWidth || 80; - // Get terminal width - more reliable method - let terminalWidth; - try { - // Try to get the actual terminal columns - terminalWidth = process.stdout.columns; - } catch (e) { - // Fallback if columns cannot be determined - log('debug', 'Could not determine terminal width, using default'); - } - // Ensure we have a reasonable default if detection fails - terminalWidth = terminalWidth || 80; + // Ensure terminal width is at least a minimum value to prevent layout issues + terminalWidth = Math.max(terminalWidth, 80); - // Ensure terminal width is at least a minimum value to prevent layout issues - terminalWidth = Math.max(terminalWidth, 80); + // Create dashboard content + const projectDashboardContent = + 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}`; - // Create dashboard content - const projectDashboardContent = - 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}`; - - 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')} + 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")} ` + - `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 - const minDashboardWidth = 50; // Minimum width for dashboard - const minDependencyWidth = 50; // Minimum width for dependency dashboard - const totalMinWidth = minDashboardWidth + minDependencyWidth + 4; // Extra 4 chars for spacing + // Calculate width for side-by-side display + // Box borders, padding take approximately 4 chars on each side + const minDashboardWidth = 50; // Minimum width for dashboard + const minDependencyWidth = 50; // Minimum width for dependency dashboard + const totalMinWidth = minDashboardWidth + minDependencyWidth + 4; // Extra 4 chars for spacing - // If terminal is wide enough, show boxes side by side with responsive widths - if (terminalWidth >= totalMinWidth) { - // Calculate widths proportionally for each box - use exact 50% width each - const availableWidth = terminalWidth; - const halfWidth = Math.floor(availableWidth / 2); + // If terminal is wide enough, show boxes side by side with responsive widths + if (terminalWidth >= totalMinWidth) { + // Calculate widths proportionally for each box - use exact 50% width each + const availableWidth = terminalWidth; + const halfWidth = Math.floor(availableWidth / 2); - // Account for border characters (2 chars on each side) - const boxContentWidth = halfWidth - 4; + // Account for border characters (2 chars on each side) + const boxContentWidth = halfWidth - 4; - // Create boxen options with precise widths - const dashboardBox = boxen(projectDashboardContent, { - padding: 1, - borderColor: 'blue', - borderStyle: 'round', - width: boxContentWidth, - dimBorder: false - }); + // Create boxen options with precise widths + const dashboardBox = boxen(projectDashboardContent, { + padding: 1, + borderColor: "blue", + borderStyle: "round", + width: boxContentWidth, + dimBorder: false, + }); - const dependencyBox = boxen(dependencyDashboardContent, { - padding: 1, - borderColor: 'magenta', - borderStyle: 'round', - width: boxContentWidth, - dimBorder: false - }); + const dependencyBox = boxen(dependencyDashboardContent, { + padding: 1, + borderColor: "magenta", + borderStyle: "round", + width: boxContentWidth, + dimBorder: false, + }); - // Create a better side-by-side layout with exact spacing - const dashboardLines = dashboardBox.split('\n'); - const dependencyLines = dependencyBox.split('\n'); + // Create a better side-by-side layout with exact spacing + 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); + // Make sure both boxes have the same height + const maxHeight = Math.max(dashboardLines.length, dependencyLines.length); - // For each line of output, pad the dashboard line to exactly halfWidth chars - // This ensures the dependency box starts at exactly the right position - 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] : ''; - // Get the dependency line (or empty string if we've run out of lines) - const depLine = i < dependencyLines.length ? dependencyLines[i] : ''; + // For each line of output, pad the dashboard line to exactly halfWidth chars + // This ensures the dependency box starts at exactly the right position + 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] : ""; + // Get the dependency line (or empty string if we've run out of lines) + 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, ' '); + // 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, " "); - // Join the lines with no space in between - combinedLines.push(paddedDashLine + depLine); - } + // Join the lines with no space in between + combinedLines.push(paddedDashLine + depLine); + } - // Join all lines and output - 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 } - }); + // Join all lines and output + 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 }, + }); - const dependencyBox = boxen(dependencyDashboardContent, { - padding: 1, - borderColor: 'magenta', - borderStyle: 'round', - margin: { top: 0, bottom: 1 } - }); + const dependencyBox = boxen(dependencyDashboardContent, { + padding: 1, + borderColor: "magenta", + borderStyle: "round", + margin: { top: 0, bottom: 1 }, + }); - // Display stacked vertically - console.log(dashboardBox); - console.log(dependencyBox); - } + // Display stacked vertically + console.log(dashboardBox); + console.log(dependencyBox); + } - if (filteredTasks.length === 0) { - console.log( - boxen( - statusFilter - ? chalk.yellow(`No tasks with status '${statusFilter}' found`) - : chalk.yellow('No tasks found'), - { padding: 1, borderColor: 'yellow', borderStyle: 'round' } - ) - ); - return; - } + if (filteredTasks.length === 0) { + console.log( + boxen( + statusFilter + ? chalk.yellow(`No tasks with status '${statusFilter}' found`) + : chalk.yellow("No tasks found"), + { padding: 1, borderColor: "yellow", borderStyle: "round" } + ) + ); + return; + } - // COMPLETELY REVISED TABLE APPROACH - // Define percentage-based column widths and calculate actual widths - // Adjust percentages based on content type and user requirements + // COMPLETELY REVISED TABLE APPROACH + // Define percentage-based column widths and calculate actual widths + // Adjust percentages based on content type and user requirements - // Adjust ID width if showing subtasks (subtask IDs are longer: e.g., "1.2") - const idWidthPct = withSubtasks ? 10 : 7; + // Adjust ID width if showing subtasks (subtask IDs are longer: e.g., "1.2") + const idWidthPct = withSubtasks ? 10 : 7; - // Calculate max status length to accommodate "in-progress" - const statusWidthPct = 15; + // Calculate max status length to accommodate "in-progress" + const statusWidthPct = 15; - // Increase priority column width as requested - const priorityWidthPct = 12; + // Increase priority column width as requested + const priorityWidthPct = 12; - // Make dependencies column smaller as requested (-20%) - const depsWidthPct = 20; + // Make dependencies column smaller as requested (-20%) + const depsWidthPct = 20; - const complexityWidthPct = 10; + const complexityWidthPct = 10; - // Calculate title/description width as remaining space (+20% from dependencies reduction) - const titleWidthPct = - 100 - - idWidthPct - - statusWidthPct - - priorityWidthPct - - depsWidthPct - - complexityWidthPct; + // Calculate title/description width as remaining space (+20% from dependencies reduction) + const titleWidthPct = + 100 - + idWidthPct - + statusWidthPct - + priorityWidthPct - + depsWidthPct - + complexityWidthPct; - // Allow 10 characters for borders and padding - const availableWidth = terminalWidth - 10; + // Allow 10 characters for borders and padding + const availableWidth = terminalWidth - 10; - // Calculate actual column widths based on percentages - const idWidth = Math.floor(availableWidth * (idWidthPct / 100)); - const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100)); - const priorityWidth = Math.floor(availableWidth * (priorityWidthPct / 100)); - const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100)); - const complexityWidth = Math.floor( - availableWidth * (complexityWidthPct / 100) - ); - const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100)); + // Calculate actual column widths based on percentages + const idWidth = Math.floor(availableWidth * (idWidthPct / 100)); + const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100)); + const priorityWidth = Math.floor(availableWidth * (priorityWidthPct / 100)); + const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100)); + const complexityWidth = Math.floor( + availableWidth * (complexityWidthPct / 100) + ); + const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100)); - // Create a table with correct borders and spacing - 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') - ], - colWidths: [ - idWidth, - titleWidth, - statusWidth, - priorityWidth, - depsWidth, - complexityWidth // Added complexity column width - ], - style: { - head: [], // No special styling for header - border: [], // No special styling for border - compact: false // Use default spacing - }, - wordWrap: true, - wrapOnWordBoundary: true - }); + // 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"), + ], + colWidths: [ + idWidth, + titleWidth, + statusWidth, + priorityWidth, + depsWidth, + complexityWidth, // Added complexity column width + ], + style: { + head: [], // No special styling for header + border: [], // No special styling for border + compact: false, // Use default spacing + }, + wordWrap: true, + wrapOnWordBoundary: true, + }); - // Process tasks for the table - filteredTasks.forEach((task) => { - // Format dependencies with status indicators (colored) - let depText = 'None'; - if (task.dependencies && task.dependencies.length > 0) { - // Use the proper formatDependenciesWithStatus function for colored status - depText = formatDependenciesWithStatus( - task.dependencies, - data.tasks, - true, - complexityReport - ); - } else { - depText = chalk.gray('None'); - } + // Process tasks for the table + filteredTasks.forEach((task) => { + // Format dependencies with status indicators (colored) + let depText = "None"; + if (task.dependencies && task.dependencies.length > 0) { + // Use the proper formatDependenciesWithStatus function for colored status + depText = formatDependenciesWithStatus( + task.dependencies, + data.tasks, + true, + complexityReport + ); + } else { + depText = chalk.gray("None"); + } - // Clean up any ANSI codes or confusing characters - const cleanTitle = task.title.replace(/\n/g, ' '); + // Clean up any ANSI codes or confusing characters + 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; + // Get priority color + const priorityColor = + { + high: chalk.red, + medium: chalk.yellow, + low: chalk.gray, + }[task.priority || "medium"] || chalk.white; - // Format status - const status = getStatusWithColor(task.status, true); + // Format status + const status = getStatusWithColor(task.status, true); - // Add the row without truncating dependencies - table.push([ - task.id.toString(), - truncate(cleanTitle, titleWidth - 3), - status, - priorityColor(truncate(task.priority || 'medium', priorityWidth - 2)), - depText, - task.complexityScore - ? getComplexityWithColor(task.complexityScore) - : chalk.gray('N/A') - ]); + // Add the row without truncating dependencies + table.push([ + task.id.toString(), + truncate(cleanTitle, titleWidth - 3), + status, + priorityColor(truncate(task.priority || "medium", priorityWidth - 2)), + depText, + task.complexityScore + ? getComplexityWithColor(task.complexityScore) + : 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'; - 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) { - 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'; + // 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"; + 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) { + 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"; - // 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}`); - } else { - return chalk.red.bold(`${task.id}.${depId}`); - } - } - } - // Default to regular task dependency - const depTask = data.tasks.find((t) => t.id === depId); - if (depTask) { - // Add complexity to depTask before checking status - addComplexityToTask(depTask, complexityReport); - const isDone = - depTask.status === 'done' || depTask.status === 'completed'; - const isInProgress = depTask.status === 'in-progress'; - // Use the same color scheme as in formatDependenciesWithStatus - if (isDone) { - return chalk.green.bold(`${depId}`); - } else if (isInProgress) { - return chalk.hex('#FFA500').bold(`${depId}`); - } else { - return chalk.red.bold(`${depId}`); - } - } - return chalk.cyan(depId.toString()); - }) - .join(', '); + // 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}`); + } else { + return chalk.red.bold(`${task.id}.${depId}`); + } + } + } + // Default to regular task dependency + const depTask = data.tasks.find((t) => t.id === depId); + if (depTask) { + // Add complexity to depTask before checking status + addComplexityToTask(depTask, complexityReport); + const isDone = + depTask.status === "done" || depTask.status === "completed"; + const isInProgress = depTask.status === "in-progress"; + // Use the same color scheme as in formatDependenciesWithStatus + if (isDone) { + return chalk.green.bold(`${depId}`); + } else if (isInProgress) { + return chalk.hex("#FFA500").bold(`${depId}`); + } else { + return chalk.red.bold(`${depId}`); + } + } + return chalk.cyan(depId.toString()); + }) + .join(", "); - subtaskDepText = formattedDeps || chalk.gray('None'); - } + subtaskDepText = formattedDeps || chalk.gray("None"); + } - // Add the subtask row without truncating dependencies - table.push([ - `${task.id}.${subtask.id}`, - chalk.dim(`└─ ${truncate(subtask.title, titleWidth - 5)}`), - getStatusWithColor(subtask.status, true), - chalk.dim('-'), - subtaskDepText, - subtask.complexityScore - ? chalk.gray(`${subtask.complexityScore}`) - : chalk.gray('N/A') - ]); - }); - } - }); + // Add the subtask row without truncating dependencies + table.push([ + `${task.id}.${subtask.id}`, + chalk.dim(`└─ ${truncate(subtask.title, titleWidth - 5)}`), + getStatusWithColor(subtask.status, true), + chalk.dim("-"), + subtaskDepText, + subtask.complexityScore + ? chalk.gray(`${subtask.complexityScore}`) + : chalk.gray("N/A"), + ]); + }); + } + }); - // Ensure we output the table even if it had to wrap - try { - console.log(table.toString()); - } catch (err) { - log('error', `Error rendering table: ${err.message}`); + // Ensure we output the table even if it had to wrap + try { + console.log(table.toString()); + } catch (err) { + 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:' - ) - ); - filteredTasks.forEach((task) => { - console.log( - `${chalk.cyan(task.id)}: ${chalk.white(task.title)} - ${getStatusWithColor(task.status)}` - ); - }); - } + // Fall back to simpler output + console.log( + chalk.yellow( + "\nFalling back to simple task list due to terminal width constraints:" + ) + ); + filteredTasks.forEach((task) => { + console.log( + `${chalk.cyan(task.id)}: ${chalk.white(task.title)} - ${getStatusWithColor(task.status)}` + ); + }); + } - // Show filter info if applied - if (statusFilter) { - console.log(chalk.yellow(`\nFiltered by status: ${statusFilter}`)); - console.log( - chalk.yellow(`Showing ${filteredTasks.length} of ${totalTasks} tasks`) - ); - } + // Show filter info if applied + if (statusFilter) { + console.log(chalk.yellow(`\nFiltered by status: ${statusFilter}`)); + console.log( + chalk.yellow(`Showing ${filteredTasks.length} of ${totalTasks} tasks`) + ); + } - // Define priority colors - const priorityColors = { - high: chalk.red.bold, - medium: chalk.yellow, - low: chalk.gray - }; + // Define priority colors + const priorityColors = { + high: chalk.red.bold, + medium: chalk.yellow, + 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 = ''; - // 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) - ); // Find the original task object - if ( - parentTaskForSubtasks && - parentTaskForSubtasks.subtasks && - parentTaskForSubtasks.subtasks.length > 0 - ) { - subtasksSection = `\n\n${chalk.white.bold('Subtasks:')}\n`; - subtasksSection += parentTaskForSubtasks.subtasks - .map((subtask) => { - // Add complexity to subtask before display - addComplexityToTask(subtask, complexityReport); - // Using a more simplified format for subtask status display - const status = subtask.status || 'pending'; - const statusColors = { - done: chalk.green, - completed: chalk.green, - pending: chalk.yellow, - 'in-progress': chalk.blue, - deferred: chalk.gray, - blocked: chalk.red, - 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'); - } + // Show next task box in a prominent color + if (nextItem) { + // Prepare subtasks section if they exist (Only tasks have .subtasks property) + 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) + ); // Find the original task object + if ( + parentTaskForSubtasks && + parentTaskForSubtasks.subtasks && + parentTaskForSubtasks.subtasks.length > 0 + ) { + subtasksSection = `\n\n${chalk.white.bold("Subtasks:")}\n`; + subtasksSection += parentTaskForSubtasks.subtasks + .map((subtask) => { + // Add complexity to subtask before display + addComplexityToTask(subtask, complexityReport); + // Using a more simplified format for subtask status display + const status = subtask.status || "pending"; + const statusColors = { + done: chalk.green, + completed: chalk.green, + pending: chalk.yellow, + "in-progress": chalk.blue, + deferred: chalk.gray, + blocked: chalk.red, + 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"); + } - console.log( - boxen( - chalk.hex('#FF8800').bold( - // Use nextItem.id and nextItem.title - `🔥 Next Task to Work On: #${nextItem.id} - ${nextItem.title}` - ) + - '\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` + - // Use nextTask.description (Note: findNextTask doesn't return description, need to fetch original task/subtask for this) - // *** Fetching original item for description and details *** - `${chalk.white('Description:')} ${getWorkItemDescription(nextItem, data.tasks)}` + - subtasksSection + // <-- Subtasks are handled above now - '\n\n' + - // Use nextItem.id - `${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}`)}`, - { - padding: { left: 2, right: 2, top: 1, bottom: 1 }, - borderColor: '#FF8800', - borderStyle: 'round', - margin: { top: 1, bottom: 1 }, - title: '⚡ RECOMMENDED NEXT TASK ⚡', - titleAlignment: 'center', - width: terminalWidth - 4, - 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.', - { - padding: 1, - 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 - } - ) - ); - } + console.log( + boxen( + chalk.hex("#FF8800").bold( + // Use nextItem.id and nextItem.title + `🔥 Next Task to Work On: #${nextItem.id} - ${nextItem.title}` + ) + + "\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` + + // Use nextTask.description (Note: findNextTask doesn't return description, need to fetch original task/subtask for this) + // *** Fetching original item for description and details *** + `${chalk.white("Description:")} ${getWorkItemDescription(nextItem, data.tasks)}` + + subtasksSection + // <-- Subtasks are handled above now + "\n\n" + + // Use nextItem.id + `${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}`)}`, + { + padding: { left: 2, right: 2, top: 1, bottom: 1 }, + borderColor: "#FF8800", + borderStyle: "round", + margin: { top: 1, bottom: 1 }, + title: "⚡ RECOMMENDED NEXT TASK ⚡", + titleAlignment: "center", + width: terminalWidth - 4, + 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.", + { + padding: 1, + 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 + } + ) + ); + } - // 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=')} to break down a task into subtasks\n` + - `${chalk.cyan('3.')} Run ${chalk.yellow('task-master set-status --id= --status=done')} to mark a task as complete`, - { - padding: 1, - borderColor: 'gray', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); - } catch (error) { - log('error', `Error listing tasks: ${error.message}`); + // 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=")} to break down a task into subtasks\n` + + `${chalk.cyan("3.")} Run ${chalk.yellow("task-master set-status --id= --status=done")} to mark a task as complete`, + { + padding: 1, + borderColor: "gray", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); + } catch (error) { + log("error", `Error listing tasks: ${error.message}`); - if (outputFormat === 'json') { - // Return structured error for JSON output - throw { - code: 'TASK_LIST_ERROR', - message: error.message, - details: error.stack - }; - } + if (outputFormat === "json") { + // Return structured error for JSON output + throw { + code: "TASK_LIST_ERROR", + message: error.message, + details: error.stack, + }; + } - console.error(chalk.red(`Error: ${error.message}`)); - process.exit(1); - } + console.error(chalk.red(`Error: ${error.message}`)); + process.exit(1); + } } // *** Helper function to get description for task or subtask *** function getWorkItemDescription(item, allTasks) { - 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.'; - } else { - // It's a top-level task - const task = allTasks.find((t) => String(t.id) === String(item.id)); - return task?.description || 'No description available.'; - } + 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."; + } else { + // It's a top-level task + const task = allTasks.find((t) => String(t.id) === String(item.id)); + return task?.description || "No description available."; + } } export default listTasks; diff --git a/scripts/modules/task-manager/set-task-status.js b/scripts/modules/task-manager/set-task-status.js index 9278fdff..7e66babd 100644 --- a/scripts/modules/task-manager/set-task-status.js +++ b/scripts/modules/task-manager/set-task-status.js @@ -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'; + isValidTaskStatus, + TASK_STATUS_OPTIONS, +} from "../../../src/constants/task-status.js"; /** * Set the status of a task @@ -22,102 +22,100 @@ import { * @returns {Object|undefined} Result object in MCP mode, undefined in CLI mode */ 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(', ')}` - ); - } - // Determine if we're in MCP mode by checking for mcpLog - const isMcpMode = !!options?.mcpLog; + try { + if (!isValidTaskStatus(newStatus)) { + throw new Error( + `Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(", ")}` + ); + } + // Determine if we're in MCP mode by checking for mcpLog + const isMcpMode = !!options?.mcpLog; - // Only display UI elements if not in MCP mode - if (!isMcpMode) { - displayBanner(); + // Only display UI elements if not in MCP mode + if (!isMcpMode) { + console.log( + boxen(chalk.white.bold(`Updating Task Status to: ${newStatus}`), { + padding: 1, + borderColor: "blue", + borderStyle: "round", + }) + ); + } - console.log( - boxen(chalk.white.bold(`Updating Task Status to: ${newStatus}`), { - padding: 1, - borderColor: 'blue', - borderStyle: 'round' - }) - ); - } + log("info", `Reading tasks from ${tasksPath}...`); + const data = readJSON(tasksPath); + if (!data || !data.tasks) { + throw new Error(`No valid tasks found in ${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 updatedTasks = []; - // Handle multiple task IDs (comma-separated) - const taskIds = taskIdInput.split(',').map((id) => id.trim()); - const updatedTasks = []; + // Update each task + for (const id of taskIds) { + await updateSingleTaskStatus(tasksPath, id, newStatus, data, !isMcpMode); + updatedTasks.push(id); + } - // Update each task - for (const id of taskIds) { - await updateSingleTaskStatus(tasksPath, id, newStatus, data, !isMcpMode); - updatedTasks.push(id); - } + // Write the updated tasks to the file + writeJSON(tasksPath, data); - // Write the updated tasks to the file - writeJSON(tasksPath, data); + // Validate dependencies after status update + log("info", "Validating dependencies after status update..."); + validateTaskDependencies(data.tasks); - // Validate dependencies after status update - log('info', 'Validating dependencies after status update...'); - validateTaskDependencies(data.tasks); + // Generate individual task files + log("info", "Regenerating task files..."); + await generateTaskFiles(tasksPath, path.dirname(tasksPath), { + mcpLog: options.mcpLog, + }); - // Generate individual task files - log('info', 'Regenerating task files...'); - await generateTaskFiles(tasksPath, path.dirname(tasksPath), { - mcpLog: options.mcpLog - }); + // Display success message - only in CLI mode + if (!isMcpMode) { + for (const id of updatedTasks) { + const task = findTaskById(data.tasks, id); + const taskName = task ? task.title : id; - // Display success message - only in CLI mode - if (!isMcpMode) { - for (const id of updatedTasks) { - const task = findTaskById(data.tasks, id); - const taskName = task ? task.title : id; + console.log( + boxen( + chalk.white.bold(`Successfully updated task ${id} status:`) + + "\n" + + `From: ${chalk.yellow(task ? task.status : "unknown")}\n` + + `To: ${chalk.green(newStatus)}`, + { padding: 1, borderColor: "green", borderStyle: "round" } + ) + ); + } + } - console.log( - boxen( - chalk.white.bold(`Successfully updated task ${id} status:`) + - '\n' + - `From: ${chalk.yellow(task ? task.status : 'unknown')}\n` + - `To: ${chalk.green(newStatus)}`, - { padding: 1, borderColor: 'green', borderStyle: 'round' } - ) - ); - } - } + // Return success value for programmatic use + return { + success: true, + updatedTasks: updatedTasks.map((id) => ({ + id, + status: newStatus, + })), + }; + } catch (error) { + log("error", `Error setting task status: ${error.message}`); - // Return success value for programmatic use - return { - success: true, - updatedTasks: updatedTasks.map((id) => ({ - id, - status: newStatus - })) - }; - } catch (error) { - log('error', `Error setting task status: ${error.message}`); + // Only show error UI in CLI mode + if (!options?.mcpLog) { + console.error(chalk.red(`Error: ${error.message}`)); - // Only show error UI in CLI mode - if (!options?.mcpLog) { - console.error(chalk.red(`Error: ${error.message}`)); + // Pass session to getDebugFlag + if (getDebugFlag(options?.session)) { + // Use getter + console.error(error); + } - // Pass session to getDebugFlag - if (getDebugFlag(options?.session)) { - // Use getter - console.error(error); - } - - process.exit(1); - } else { - // In MCP mode, throw the error for the caller to handle - throw error; - } - } + process.exit(1); + } else { + // In MCP mode, throw the error for the caller to handle + throw error; + } + } } export default setTaskStatus; diff --git a/scripts/modules/ui.js b/scripts/modules/ui.js index 1118f8c7..9c1307ca 100644 --- a/scripts/modules/ui.js +++ b/scripts/modules/ui.js @@ -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 @@ -767,8 +827,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) { @@ -1039,8 +1097,6 @@ async function displayTaskById( complexityReportPath = null, statusFilter = null ) { - displayBanner(); - // Read the tasks file const data = readJSON(tasksPath); if (!data || !data.tasks) { @@ -1495,8 +1551,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( @@ -2094,4 +2148,8 @@ export { displayModelConfiguration, displayAvailableModels, displayAiUsageSummary, + succeedLoadingIndicator, + failLoadingIndicator, + warnLoadingIndicator, + infoLoadingIndicator, };