From 15ad34928df4b68efedb121a958e8b9c4d722c84 Mon Sep 17 00:00:00 2001 From: Eyal Toledano Date: Sun, 25 May 2025 18:03:43 -0400 Subject: [PATCH] fix(move-task): Fix critical bugs in task move functionality - Fixed parent-to-parent task moves where original task would remain as duplicate - Fixed moving tasks to become subtasks of empty parents (validation errors) - Fixed moving subtasks between different parent tasks - Improved comma-separated batch moves with proper error handling - Updated MCP tool to use core logic instead of custom implementation - Resolves task duplication issues and enables proper task hierarchy reorganization --- .changeset/cold-pears-poke.md | 13 +++ .../src/core/direct-functions/move-task.js | 12 ++- mcp-server/src/tools/move-task.js | 89 +++-------------- scripts/modules/task-manager/move-task.js | 97 ++++++++++++++++++- tasks/{task_096.txt => task_085.txt} | 12 +-- tasks/{task_098.txt => task_086.txt} | 2 +- tasks/tasks.json | 18 ++-- 7 files changed, 144 insertions(+), 99 deletions(-) create mode 100644 .changeset/cold-pears-poke.md rename tasks/{task_096.txt => task_085.txt} (99%) rename tasks/{task_098.txt => task_086.txt} (99%) diff --git a/.changeset/cold-pears-poke.md b/.changeset/cold-pears-poke.md new file mode 100644 index 00000000..95e62116 --- /dev/null +++ b/.changeset/cold-pears-poke.md @@ -0,0 +1,13 @@ +--- +'task-master-ai': patch +--- + +Fix critical bugs in task move functionality: + +- **Fixed moving tasks to become subtasks of empty parents**: When moving a task to become a subtask of a parent that had no existing subtasks (e.g., task 89 → task 98.1), the operation would fail with validation errors. + +- **Fixed moving subtasks between parents**: Subtasks can now be properly moved between different parent tasks, including to parents that previously had no subtasks. + +- **Improved comma-separated batch moves**: Multiple tasks can now be moved simultaneously using comma-separated IDs (e.g., "88,90" → "92,93") with proper error handling and atomic operations. + +These fixes enables proper task hierarchy reorganization for corner cases that were previously broken. diff --git a/mcp-server/src/core/direct-functions/move-task.js b/mcp-server/src/core/direct-functions/move-task.js index d121c68d..95afc30c 100644 --- a/mcp-server/src/core/direct-functions/move-task.js +++ b/mcp-server/src/core/direct-functions/move-task.js @@ -13,10 +13,11 @@ import { * Move a task or subtask to a new position * @param {Object} args - Function arguments * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file - * @param {string} args.sourceId - ID of the task/subtask to move (e.g., '5' or '5.2') - * @param {string} args.destinationId - ID of the destination (e.g., '7' or '7.3') + * @param {string} args.sourceId - ID of the task/subtask to move (e.g., '5' or '5.2' or '5,6,7') + * @param {string} args.destinationId - ID of the destination (e.g., '7' or '7.3' or '7,8,9') * @param {string} args.file - Alternative path to the tasks.json file * @param {string} args.projectRoot - Project root directory + * @param {boolean} args.generateFiles - Whether to regenerate task files after moving (default: true) * @param {Object} log - Logger object * @returns {Promise<{success: boolean, data?: Object, error?: Object}>} */ @@ -64,12 +65,13 @@ export async function moveTaskDirect(args, log, context = {}) { // Enable silent mode to prevent console output during MCP operation enableSilentMode(); - // Call the core moveTask function, always generate files + // Call the core moveTask function with file generation control + const generateFiles = args.generateFiles !== false; // Default to true const result = await moveTask( tasksPath, args.sourceId, args.destinationId, - true + generateFiles ); // Restore console output @@ -78,7 +80,7 @@ export async function moveTaskDirect(args, log, context = {}) { return { success: true, data: { - movedTask: result.movedTask, + ...result, message: `Successfully moved task/subtask ${args.sourceId} to ${args.destinationId}` } }; diff --git a/mcp-server/src/tools/move-task.js b/mcp-server/src/tools/move-task.js index afbe2f3b..d111efcb 100644 --- a/mcp-server/src/tools/move-task.js +++ b/mcp-server/src/tools/move-task.js @@ -41,83 +41,20 @@ export function registerMoveTaskTool(server) { }), execute: withNormalizedProjectRoot(async (args, { log, session }) => { try { - // Find tasks.json path if not provided - let tasksJsonPath = args.file; + // Let the core logic handle comma-separated IDs and validation + const result = await moveTaskDirect( + { + sourceId: args.from, + destinationId: args.to, + file: args.file, + projectRoot: args.projectRoot, + generateFiles: true // Always generate files for MCP operations + }, + log, + { session } + ); - if (!tasksJsonPath) { - tasksJsonPath = findTasksJsonPath(args, log); - } - - // Parse comma-separated IDs - const fromIds = args.from.split(',').map((id) => id.trim()); - const toIds = args.to.split(',').map((id) => id.trim()); - - // Validate matching IDs count - if (fromIds.length !== toIds.length) { - return createErrorResponse( - 'The number of source and destination IDs must match', - 'MISMATCHED_ID_COUNT' - ); - } - - // If moving multiple tasks - if (fromIds.length > 1) { - const results = []; - // Move tasks one by one, only generate files on the last move - for (let i = 0; i < fromIds.length; i++) { - const fromId = fromIds[i]; - const toId = toIds[i]; - - // Skip if source and destination are the same - if (fromId === toId) { - log.info(`Skipping ${fromId} -> ${toId} (same ID)`); - continue; - } - - const shouldGenerateFiles = i === fromIds.length - 1; - const result = await moveTaskDirect( - { - sourceId: fromId, - destinationId: toId, - tasksJsonPath, - projectRoot: args.projectRoot - }, - log, - { session } - ); - - if (!result.success) { - log.error( - `Failed to move ${fromId} to ${toId}: ${result.error.message}` - ); - } else { - results.push(result.data); - } - } - - return { - success: true, - data: { - moves: results, - message: `Successfully moved ${results.length} tasks` - } - }; - } else { - // Moving a single task - return handleApiResult( - await moveTaskDirect( - { - sourceId: args.from, - destinationId: args.to, - tasksJsonPath, - projectRoot: args.projectRoot - }, - log, - { session } - ), - log - ); - } + return handleApiResult(result, log); } catch (error) { return createErrorResponse( `Failed to move task: ${error.message}`, diff --git a/scripts/modules/task-manager/move-task.js b/scripts/modules/task-manager/move-task.js index 1a871a13..5ab88134 100644 --- a/scripts/modules/task-manager/move-task.js +++ b/scripts/modules/task-manager/move-task.js @@ -4,14 +4,107 @@ import { isTaskDependentOn } from '../task-manager.js'; import generateTaskFiles from './generate-task-files.js'; /** - * Move a task or subtask to a new position + * Move one or more tasks/subtasks to new positions + * @param {string} tasksPath - Path to tasks.json file + * @param {string} sourceId - ID(s) of the task/subtask to move (e.g., '5' or '5.2' or '5,6,7') + * @param {string} destinationId - ID(s) of the destination (e.g., '7' or '7.3' or '7,8,9') + * @param {boolean} generateFiles - Whether to regenerate task files after moving + * @returns {Object} Result object with moved task details + */ +async function moveTask( + tasksPath, + sourceId, + destinationId, + generateFiles = true +) { + // Check if we have comma-separated IDs (multiple moves) + const sourceIds = sourceId.split(',').map((id) => id.trim()); + const destinationIds = destinationId.split(',').map((id) => id.trim()); + + // If multiple IDs, validate they match in count + if (sourceIds.length > 1 || destinationIds.length > 1) { + if (sourceIds.length !== destinationIds.length) { + throw new Error( + `Number of source IDs (${sourceIds.length}) must match number of destination IDs (${destinationIds.length})` + ); + } + + // Perform multiple moves + return await moveMultipleTasks( + tasksPath, + sourceIds, + destinationIds, + generateFiles + ); + } + + // Single move - use existing logic + return await moveSingleTask( + tasksPath, + sourceId, + destinationId, + generateFiles + ); +} + +/** + * Move multiple tasks/subtasks to new positions + * @param {string} tasksPath - Path to tasks.json file + * @param {string[]} sourceIds - Array of source IDs + * @param {string[]} destinationIds - Array of destination IDs + * @param {boolean} generateFiles - Whether to regenerate task files after moving + * @returns {Object} Result object with moved task details + */ +async function moveMultipleTasks( + tasksPath, + sourceIds, + destinationIds, + generateFiles = true +) { + try { + log( + 'info', + `Moving multiple tasks/subtasks: ${sourceIds.join(', ')} to ${destinationIds.join(', ')}...` + ); + + const results = []; + + // Perform moves one by one, but don't regenerate files until the end + for (let i = 0; i < sourceIds.length; i++) { + const result = await moveSingleTask( + tasksPath, + sourceIds[i], + destinationIds[i], + false + ); + results.push(result); + } + + // Generate task files once at the end if requested + if (generateFiles) { + log('info', 'Regenerating task files...'); + await generateTaskFiles(tasksPath, path.dirname(tasksPath)); + } + + return { + message: `Successfully moved ${sourceIds.length} tasks/subtasks`, + moves: results + }; + } catch (error) { + log('error', `Error moving multiple tasks/subtasks: ${error.message}`); + throw error; + } +} + +/** + * Move a single task or subtask to a new position * @param {string} tasksPath - Path to tasks.json file * @param {string} sourceId - ID of the task/subtask to move (e.g., '5' or '5.2') * @param {string} destinationId - ID of the destination (e.g., '7' or '7.3') * @param {boolean} generateFiles - Whether to regenerate task files after moving * @returns {Object} Result object with moved task details */ -async function moveTask( +async function moveSingleTask( tasksPath, sourceId, destinationId, diff --git a/tasks/task_096.txt b/tasks/task_085.txt similarity index 99% rename from tasks/task_096.txt rename to tasks/task_085.txt index 4507c291..d3940757 100644 --- a/tasks/task_096.txt +++ b/tasks/task_085.txt @@ -1,4 +1,4 @@ -# Task ID: 96 +# Task ID: 85 # Title: Implement Dynamic Help Menu Generation from CLI Commands # Status: pending # Dependencies: 2, 4 @@ -237,7 +237,7 @@ This implementation will create a self-documenting CLI that maintains accuracy a - Performance testing with large command sets ## 2. Create Intelligent Command Categorization System [pending] -### Dependencies: 96.1 +### Dependencies: 85.1 ### Description: Implement smart categorization logic to group commands into logical categories for the help menu ### Details: ## Implementation Requirements @@ -369,7 +369,7 @@ function categorizeCommands(commandMetadata) { - Validate category completeness and no duplicates ## 3. Build Dynamic Help Content Generator [pending] -### Dependencies: 96.2 +### Dependencies: 85.2 ### Description: Create the core help content generation system that formats command metadata into user-friendly help text ### Details: ## Implementation Requirements @@ -505,7 +505,7 @@ function generateCategorySection(categoryName, categoryData) { - Performance testing with large command sets ## 4. Integrate Dynamic Help System with Existing CLI [pending] -### Dependencies: 96.3 +### Dependencies: 85.3 ### Description: Replace the static help system with the new dynamic help generation and ensure seamless integration ### Details: ## Implementation Requirements @@ -656,7 +656,7 @@ export function setProgramInstance(program) { 5. **Phase 5**: Add enhanced help features (search, filtering, etc.) ## 5. Add Enhanced Help Features and Search Functionality [pending] -### Dependencies: 96.4 +### Dependencies: 85.4 ### Description: Implement advanced help features including command search, category filtering, and contextual help suggestions ### Details: ## Implementation Requirements @@ -920,7 +920,7 @@ function enhanceErrorWithHelp(error, commandName, commandMetadata) { - Integration examples ## 6. Create Comprehensive Testing Suite and Documentation [pending] -### Dependencies: 96.5 +### Dependencies: 85.5 ### Description: Implement thorough testing for the dynamic help system and update all relevant documentation ### Details: ## Implementation Requirements diff --git a/tasks/task_098.txt b/tasks/task_086.txt similarity index 99% rename from tasks/task_098.txt rename to tasks/task_086.txt index 3b14fbd3..154d1801 100644 --- a/tasks/task_098.txt +++ b/tasks/task_086.txt @@ -1,4 +1,4 @@ -# Task ID: 98 +# Task ID: 86 # Title: Implement Separate Context Window and Output Token Limits # Status: pending # Dependencies: None diff --git a/tasks/tasks.json b/tasks/tasks.json index 1271d31d..77f92fa1 100644 --- a/tasks/tasks.json +++ b/tasks/tasks.json @@ -5508,7 +5508,7 @@ ] }, { - "id": 96, + "id": 85, "title": "Implement Dynamic Help Menu Generation from CLI Commands", "description": "Transform the static help menu in ui.js into a dynamic system that automatically generates help content by introspecting the actual CLI commands, options, and flags defined in commands.js. This ensures the help menu stays synchronized with command implementations and reduces maintenance overhead.", "details": "## Core Problem Statement\n\nThe current help menu in `displayHelp()` function (ui.js:434-734) is hardcoded with static command information that can become outdated when:\n\n1. **Command Changes**: New options/flags are added to existing commands\n2. **New Commands**: New commands are added to commands.js but not reflected in help\n3. **Command Removal**: Commands are removed but help text remains\n4. **Inconsistent Documentation**: Help text doesn't match actual command behavior\n5. **Maintenance Burden**: Developers must remember to update help when modifying commands\n\n## Technical Implementation Requirements\n\n### 1. Command Introspection System\n- **Extract Command Metadata**: Parse Commander.js program instance to extract:\n - Command names and aliases\n - Command descriptions\n - All options/flags with their descriptions and default values\n - Required vs optional parameters\n - Argument specifications\n- **Command Categorization**: Implement intelligent categorization based on:\n - Command name patterns (e.g., 'add-*', 'remove-*', 'set-*')\n - Command descriptions containing keywords\n - Manual category overrides for edge cases\n- **Validation**: Ensure all registered commands are captured and categorized\n\n### 2. Dynamic Help Generation Engine\n- **Template System**: Create flexible templates for:\n - Category headers with consistent styling\n - Command entries with proper formatting\n - Option/flag documentation with type information\n - Example usage generation\n- **Formatting Logic**: Implement dynamic column width calculation based on:\n - Terminal width detection\n - Content length analysis\n - Responsive layout adjustments\n- **Content Optimization**: Handle text wrapping, truncation, and spacing automatically\n\n### 3. Enhanced Command Documentation\n- **Auto-Generated Examples**: Create realistic usage examples by:\n - Combining command names with common option patterns\n - Using project-specific values (task IDs, file paths)\n - Showing both simple and complex usage scenarios\n- **Option Details**: Display comprehensive option information:\n - Short and long flag variants (-f, --file)\n - Data types and format requirements\n - Default values and behavior\n - Required vs optional indicators\n- **Cross-References**: Add intelligent linking between related commands\n\n### 4. Integration Points\n- **Commands.js Integration**: \n - Access the programInstance after all commands are registered\n - Extract metadata without affecting command functionality\n - Handle edge cases like hidden commands or aliases\n- **UI.js Refactoring**:\n - Replace static commandCategories array with dynamic generation\n - Maintain existing visual styling and layout\n - Preserve terminal width responsiveness\n - Keep configuration and quick start sections\n\n### 5. Category Classification Logic\nImplement smart categorization rules:\n```javascript\nconst categoryRules = {\n 'Project Setup & Configuration': ['init', 'models'],\n 'Task Generation': ['parse-prd', 'generate'],\n 'Task Management': ['list', 'set-status', 'update', 'add-task', 'remove-task'],\n 'Subtask Management': ['add-subtask', 'remove-subtask', 'clear-subtasks'],\n 'Task Analysis & Breakdown': ['analyze-complexity', 'complexity-report', 'expand', 'research'],\n 'Task Navigation & Viewing': ['next', 'show'],\n 'Dependency Management': ['add-dependency', 'remove-dependency', 'validate-dependencies', 'fix-dependencies']\n};\n```\n\n### 6. Error Handling and Fallbacks\n- **Graceful Degradation**: Fall back to static help if introspection fails\n- **Missing Information**: Handle commands with incomplete metadata\n- **Performance Considerations**: Cache generated help content when possible\n- **Debug Mode**: Provide verbose output for troubleshooting categorization\n\n## Implementation Architecture\n\n### Core Functions to Implement:\n1. **`extractCommandMetadata(programInstance)`**\n - Parse Commander.js instance\n - Extract all command and option information\n - Return structured metadata object\n\n2. **`categorizeCommands(commandMetadata)`**\n - Apply categorization rules\n - Handle special cases and overrides\n - Return categorized command structure\n\n3. **`generateDynamicHelp(categorizedCommands)`**\n - Create formatted help content\n - Apply consistent styling\n - Handle responsive layout\n\n4. **`displayDynamicHelp(programInstance)`**\n - Replace current displayHelp() function\n - Integrate with existing banner and footer content\n - Maintain backward compatibility\n\n### File Structure Changes:\n- **ui.js**: Replace static help with dynamic generation\n- **commands.js**: Ensure all commands have proper descriptions and option documentation\n- **New utility functions**: Add command introspection helpers\n\n## Testing Requirements\n\n### Unit Tests:\n- Command metadata extraction accuracy\n- Categorization logic correctness\n- Help content generation formatting\n- Terminal width responsiveness\n\n### Integration Tests:\n- Full help menu generation from actual commands\n- Consistency between help and actual command behavior\n- Performance with large numbers of commands\n\n### Manual Testing:\n- Visual verification of help output\n- Terminal width adaptation testing\n- Comparison with current static help for completeness\n\n## Benefits\n\n1. **Automatic Synchronization**: Help always reflects actual command state\n2. **Reduced Maintenance**: No manual help updates needed for command changes\n3. **Consistency**: Guaranteed alignment between help and implementation\n4. **Extensibility**: Easy to add new categorization rules or formatting\n5. **Accuracy**: Eliminates human error in help documentation\n6. **Developer Experience**: Faster development with automatic documentation\n\n## Migration Strategy\n\n1. **Phase 1**: Implement introspection system alongside existing static help\n2. **Phase 2**: Add categorization and dynamic generation\n3. **Phase 3**: Replace static help with dynamic system\n4. **Phase 4**: Remove static command definitions and add validation tests\n\nThis implementation will create a self-documenting CLI that maintains accuracy and reduces the burden on developers to manually maintain help documentation.", @@ -5527,7 +5527,7 @@ "details": "## Implementation Requirements\n\n### Core Function: `extractCommandMetadata(programInstance)`\n\n**Location**: Add to `ui.js` or create new `help-utils.js` module\n\n**Functionality**:\n1. **Command Discovery**:\n - Iterate through `programInstance.commands` array\n - Extract command names, aliases, and descriptions\n - Handle subcommands and nested command structures\n - Filter out hidden or internal commands\n\n2. **Option Extraction**:\n - Parse `command.options` array for each command\n - Extract short flags (-f), long flags (--file), descriptions\n - Identify required vs optional parameters\n - Capture default values and data types\n - Handle boolean flags vs value-accepting options\n\n3. **Argument Processing**:\n - Extract positional arguments and their descriptions\n - Identify required vs optional arguments\n - Handle variadic arguments (e.g., [files...])\n\n4. **Metadata Structure**:\n```javascript\n{\n commandName: {\n name: 'command-name',\n aliases: ['alias1', 'alias2'],\n description: 'Command description',\n usage: 'command-name [options] ',\n options: [\n {\n flags: '-f, --file ',\n description: 'File path description',\n required: false,\n defaultValue: 'default.json',\n type: 'string'\n }\n ],\n arguments: [\n {\n name: 'id',\n description: 'Task ID',\n required: true,\n variadic: false\n }\n ]\n }\n}\n```\n\n### Technical Implementation:\n1. **Commander.js API Usage**:\n - Access `command._name`, `command._description`\n - Parse `command.options` for option metadata\n - Handle `command._args` for positional arguments\n - Use `command._aliases` for command aliases\n\n2. **Option Parsing Logic**:\n - Parse option flags using regex to separate short/long forms\n - Detect required parameters using `<>` vs optional `[]`\n - Extract default values from option configurations\n - Identify boolean flags vs value-accepting options\n\n3. **Error Handling**:\n - Handle commands with missing descriptions\n - Deal with malformed option definitions\n - Provide fallbacks for incomplete metadata\n - Log warnings for problematic command definitions\n\n### Testing Requirements:\n- Unit tests for metadata extraction accuracy\n- Test with various command configurations\n- Verify handling of edge cases (missing descriptions, complex options)\n- Performance testing with large command sets", "status": "pending", "dependencies": [], - "parentTaskId": 96 + "parentTaskId": 85 }, { "id": 2, @@ -5538,7 +5538,7 @@ "dependencies": [ 1 ], - "parentTaskId": 96 + "parentTaskId": 85 }, { "id": 3, @@ -5549,7 +5549,7 @@ "dependencies": [ 2 ], - "parentTaskId": 96 + "parentTaskId": 85 }, { "id": 4, @@ -5560,7 +5560,7 @@ "dependencies": [ 3 ], - "parentTaskId": 96 + "parentTaskId": 85 }, { "id": 5, @@ -5571,7 +5571,7 @@ "dependencies": [ 4 ], - "parentTaskId": 96 + "parentTaskId": 85 }, { "id": 6, @@ -5582,12 +5582,12 @@ "dependencies": [ 5 ], - "parentTaskId": 96 + "parentTaskId": 85 } ] }, { - "id": 98, + "id": 86, "title": "Implement Separate Context Window and Output Token Limits", "description": "Replace the ambiguous MAX_TOKENS configuration with separate contextWindowTokens and maxOutputTokens fields to properly handle model token limits and enable dynamic token allocation.", "details": "Currently, the MAX_TOKENS configuration entry is ambiguous and doesn't properly differentiate between:\n1. Context window tokens (total input + output capacity)\n2. Maximum output tokens (generation limit)\n\nThis causes issues where:\n- The system can't properly validate prompt lengths against model capabilities\n- Output token allocation is not optimized based on input length\n- Different models with different token architectures are handled inconsistently\n\nThis epic will implement a comprehensive solution that:\n- Updates supported-models.json with accurate contextWindowTokens and maxOutputTokens for each model\n- Modifies config-manager.js to use separate maxInputTokens and maxOutputTokens in role configurations\n- Implements a token counting utility for accurate prompt measurement\n- Updates ai-services-unified.js to dynamically calculate available output tokens\n- Provides migration guidance and validation for existing configurations\n- Adds comprehensive error handling and validation throughout the system\n\nThe end result will be more precise token management, better cost control, and reduced likelihood of hitting model context limits.", @@ -5606,7 +5606,7 @@ "dependencies": [], "status": "pending", "subtasks": [], - "parentTaskId": 98 + "parentTaskId": 86 } ] }