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:
13
.changeset/cold-pears-poke.md
Normal file
13
.changeset/cold-pears-poke.md
Normal 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.
|
||||
@@ -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}`
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -1,4 +1,4 @@
|
||||
# Task ID: 98
|
||||
# Task ID: 86
|
||||
# Title: Implement Separate Context Window and Output Token Limits
|
||||
# Status: pending
|
||||
# Dependencies: None
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user