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
This commit is contained in:
Eyal Toledano
2025-05-25 18:03:43 -04:00
parent f74d639110
commit 15ad34928d
7 changed files with 144 additions and 99 deletions

View File

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

View File

@@ -13,10 +13,11 @@ import {
* Move a task or subtask to a new position * Move a task or subtask to a new position
* @param {Object} args - Function arguments * @param {Object} args - Function arguments
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file * @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.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') * @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.file - Alternative path to the tasks.json file
* @param {string} args.projectRoot - Project root directory * @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 * @param {Object} log - Logger object
* @returns {Promise<{success: boolean, data?: Object, error?: 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 // Enable silent mode to prevent console output during MCP operation
enableSilentMode(); 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( const result = await moveTask(
tasksPath, tasksPath,
args.sourceId, args.sourceId,
args.destinationId, args.destinationId,
true generateFiles
); );
// Restore console output // Restore console output
@@ -78,7 +80,7 @@ export async function moveTaskDirect(args, log, context = {}) {
return { return {
success: true, success: true,
data: { data: {
movedTask: result.movedTask, ...result,
message: `Successfully moved task/subtask ${args.sourceId} to ${args.destinationId}` message: `Successfully moved task/subtask ${args.sourceId} to ${args.destinationId}`
} }
}; };

View File

@@ -41,83 +41,20 @@ export function registerMoveTaskTool(server) {
}), }),
execute: withNormalizedProjectRoot(async (args, { log, session }) => { execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try { try {
// Find tasks.json path if not provided // Let the core logic handle comma-separated IDs and validation
let tasksJsonPath = args.file; 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) { return handleApiResult(result, log);
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
);
}
} catch (error) { } catch (error) {
return createErrorResponse( return createErrorResponse(
`Failed to move task: ${error.message}`, `Failed to move task: ${error.message}`,

View File

@@ -4,14 +4,107 @@ import { isTaskDependentOn } from '../task-manager.js';
import generateTaskFiles from './generate-task-files.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} tasksPath - Path to tasks.json file
* @param {string} sourceId - ID of the task/subtask to move (e.g., '5' or '5.2') * @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 {string} destinationId - ID of the destination (e.g., '7' or '7.3')
* @param {boolean} generateFiles - Whether to regenerate task files after moving * @param {boolean} generateFiles - Whether to regenerate task files after moving
* @returns {Object} Result object with moved task details * @returns {Object} Result object with moved task details
*/ */
async function moveTask( async function moveSingleTask(
tasksPath, tasksPath,
sourceId, sourceId,
destinationId, destinationId,

View File

@@ -1,4 +1,4 @@
# Task ID: 96 # Task ID: 85
# Title: Implement Dynamic Help Menu Generation from CLI Commands # Title: Implement Dynamic Help Menu Generation from CLI Commands
# Status: pending # Status: pending
# Dependencies: 2, 4 # 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 - Performance testing with large command sets
## 2. Create Intelligent Command Categorization System [pending] ## 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 ### Description: Implement smart categorization logic to group commands into logical categories for the help menu
### Details: ### Details:
## Implementation Requirements ## Implementation Requirements
@@ -369,7 +369,7 @@ function categorizeCommands(commandMetadata) {
- Validate category completeness and no duplicates - Validate category completeness and no duplicates
## 3. Build Dynamic Help Content Generator [pending] ## 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 ### Description: Create the core help content generation system that formats command metadata into user-friendly help text
### Details: ### Details:
## Implementation Requirements ## Implementation Requirements
@@ -505,7 +505,7 @@ function generateCategorySection(categoryName, categoryData) {
- Performance testing with large command sets - Performance testing with large command sets
## 4. Integrate Dynamic Help System with Existing CLI [pending] ## 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 ### Description: Replace the static help system with the new dynamic help generation and ensure seamless integration
### Details: ### Details:
## Implementation Requirements ## Implementation Requirements
@@ -656,7 +656,7 @@ export function setProgramInstance(program) {
5. **Phase 5**: Add enhanced help features (search, filtering, etc.) 5. **Phase 5**: Add enhanced help features (search, filtering, etc.)
## 5. Add Enhanced Help Features and Search Functionality [pending] ## 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 ### Description: Implement advanced help features including command search, category filtering, and contextual help suggestions
### Details: ### Details:
## Implementation Requirements ## Implementation Requirements
@@ -920,7 +920,7 @@ function enhanceErrorWithHelp(error, commandName, commandMetadata) {
- Integration examples - Integration examples
## 6. Create Comprehensive Testing Suite and Documentation [pending] ## 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 ### Description: Implement thorough testing for the dynamic help system and update all relevant documentation
### Details: ### Details:
## Implementation Requirements ## Implementation Requirements

View File

@@ -1,4 +1,4 @@
# Task ID: 98 # Task ID: 86
# Title: Implement Separate Context Window and Output Token Limits # Title: Implement Separate Context Window and Output Token Limits
# Status: pending # Status: pending
# Dependencies: None # Dependencies: None

File diff suppressed because one or more lines are too long