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
|
* 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}`
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}`,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
Reference in New Issue
Block a user