fix: Critical writeJSON Context Fixes - Prevent Tag Corruption (#910)

* feat(tasks): Fix critical tag corruption bug in task management

- Fixed missing context parameters in writeJSON calls across add-task, remove-task, and add-subtask functions
- Added projectRoot and tag parameters to prevent data corruption in multi-tag environments
- Re-enabled generateTaskFiles calls to ensure markdown files are updated after operations
- Enhanced add_subtask MCP tool with tag parameter support
- Refactored addSubtaskDirect function to properly pass context to core logic
- Streamlined codebase by removing deprecated functionality

This resolves the critical bug where task operations in one tag context would corrupt or delete tasks from other tags in tasks.json.

* feat(task-manager): Enhance addSubtask with current tag support

- Added `getCurrentTag` utility to retrieve the current tag context for task operations.
- Updated `addSubtask` to use the current tag when reading and writing tasks, ensuring proper context handling.
- Refactored tests to accommodate changes in the `addSubtask` function, ensuring accurate mock implementations and expectations.
- Cleaned up test cases for better readability and maintainability.

This improves task management by preventing tag-related data corruption and enhances the overall functionality of the task manager.

* feat(remove-task): Add tag support for task removal and enhance error handling

- Introduced `tag` parameter in `removeTaskDirect` to specify context for task operations, improving multi-tag support.
- Updated logging to include tag context in messages for better traceability.
- Refactored task removal logic to streamline the process and improve error reporting.
- Added comprehensive unit tests to validate tag handling and ensure robust error management.

This enhancement prevents task data corruption across different tags and improves the overall reliability of the task management system.

* feat(add-task): Add projectRoot and tag parameters to addTask tests

- Updated `addTask` unit tests to include `projectRoot` and `tag` parameters for better context handling.
- Enhanced test cases to ensure accurate expectations and improve overall test coverage.

This change aligns with recent enhancements in task management, ensuring consistency across task operations.

* feat(set-task-status): Add tag parameter support and enhance task status handling

- Introduced `tag` parameter in `setTaskStatusDirect` and related functions to improve context management in multi-tag environments.
- Updated `writeJSON` calls to ensure task data integrity across different tags.
- Enhanced unit tests to validate tag preservation during task status updates, ensuring robust functionality.

This change aligns with recent improvements in task management, preventing data corruption and enhancing overall reliability.

* feat(tag-management): Enhance writeJSON calls to preserve tag context

- Updated `writeJSON` calls in `createTag`, `deleteTag`, `renameTag`, `copyTag`, and `enhanceTagsWithMetadata` to include `projectRoot` for better context management and to prevent tag corruption.
- Added comprehensive unit tests for tag management functions to ensure data integrity and proper tag handling during operations.

This change improves the reliability of tag management by ensuring that operations do not corrupt existing tags and maintains the overall structure of the task data.

* feat(expand-task): Update writeJSON to include projectRoot and tag context

- Modified `writeJSON` call in `expandTaskDirect` to pass `projectRoot` and `tag` parameters, ensuring proper context management when saving tasks.json.
- This change aligns with recent enhancements in task management, preventing potential data corruption and improving overall reliability.

* feat(fix-dependencies): Add projectRoot and tag parameters for enhanced context management

- Updated `fixDependenciesDirect` and `registerFixDependenciesTool` to include `projectRoot` and `tag` parameters, improving context handling during dependency fixes.
- Introduced a new unit test for `fixDependenciesCommand` to ensure proper preservation of projectRoot and tag data in JSON outputs.

This change enhances the reliability of dependency management by ensuring that context is maintained across operations, preventing potential data issues.

* fix(context): propagate projectRoot and tag through dependency, expansion, status-update and tag-management commands to prevent cross-tag data corruption

* test(fix-dependencies): Enhance unit tests for fixDependenciesCommand

- Refactored tests to use unstable mocks for utils, ui, and task-manager modules, improving isolation and reliability.
- Added checks for process.exit to ensure proper handling of invalid data scenarios.
- Updated test cases to verify writeJSON calls with projectRoot and tag parameters, ensuring accurate context preservation during dependency fixes.

This change strengthens the test suite for dependency management, ensuring robust functionality and preventing potential data issues.

* chore(plan): remove outdated fix plan for `writeJSON` context parameters
This commit is contained in:
Parthy
2025-07-02 21:45:10 +02:00
committed by GitHub
parent 43e0025f4c
commit 2852149a47
22 changed files with 1166 additions and 389 deletions

View File

@@ -20,6 +20,8 @@ import {
* @param {string} [args.status] - Status for new subtask (default: 'pending')
* @param {string} [args.dependencies] - Comma-separated list of dependency IDs
* @param {boolean} [args.skipGenerate] - Skip regenerating task files
* @param {string} [args.projectRoot] - Project root directory
* @param {string} [args.tag] - Tag for the task
* @param {Object} log - Logger object
* @returns {Promise<{success: boolean, data?: Object, error?: string}>}
*/
@@ -34,7 +36,9 @@ export async function addSubtaskDirect(args, log) {
details,
status,
dependencies: dependenciesStr,
skipGenerate
skipGenerate,
projectRoot,
tag
} = args;
try {
log.info(`Adding subtask with args: ${JSON.stringify(args)}`);
@@ -96,6 +100,8 @@ export async function addSubtaskDirect(args, log) {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
const context = { projectRoot, tag };
// Case 1: Convert existing task to subtask
if (existingTaskId) {
log.info(`Converting task ${existingTaskId} to a subtask of ${parentId}`);
@@ -104,7 +110,8 @@ export async function addSubtaskDirect(args, log) {
parentId,
existingTaskId,
null,
generateFiles
generateFiles,
context
);
// Restore normal logging
@@ -135,7 +142,8 @@ export async function addSubtaskDirect(args, log) {
parentId,
null,
newSubtaskData,
generateFiles
generateFiles,
context
);
// Restore normal logging

View File

@@ -171,8 +171,8 @@ export async function expandTaskDirect(args, log, context = {}) {
task.subtasks = [];
}
// Save tasks.json with potentially empty subtasks array
writeJSON(tasksPath, data);
// Save tasks.json with potentially empty subtasks array and proper context
writeJSON(tasksPath, data, projectRoot, tag);
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);

View File

@@ -13,12 +13,14 @@ import fs from 'fs';
* Fix invalid dependencies in tasks.json automatically
* @param {Object} args - Function arguments
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
* @param {string} args.projectRoot - Project root directory
* @param {string} args.tag - Tag for the project
* @param {Object} log - Logger object
* @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
*/
export async function fixDependenciesDirect(args, log) {
// Destructure expected args
const { tasksJsonPath } = args;
const { tasksJsonPath, projectRoot, tag } = args;
try {
log.info(`Fixing invalid dependencies in tasks: ${tasksJsonPath}`);
@@ -51,8 +53,10 @@ export async function fixDependenciesDirect(args, log) {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Call the original command function using the provided path
await fixDependenciesCommand(tasksPath);
// Call the original command function using the provided path and proper context
await fixDependenciesCommand(tasksPath, {
context: { projectRoot, tag }
});
// Restore normal logging
disableSilentMode();
@@ -61,7 +65,8 @@ export async function fixDependenciesDirect(args, log) {
success: true,
data: {
message: 'Dependencies fixed successfully',
tasksPath
tasksPath,
tag: tag || 'master'
}
};
} catch (error) {

View File

@@ -20,12 +20,13 @@ import {
* @param {Object} args - Command arguments
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
* @param {string} args.id - The ID(s) of the task(s) or subtask(s) to remove (comma-separated for multiple).
* @param {string} [args.tag] - Tag context to operate on (defaults to current active tag).
* @param {Object} log - Logger object
* @returns {Promise<Object>} - Remove task result { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function removeTaskDirect(args, log, context = {}) {
// Destructure expected args
const { tasksJsonPath, id, projectRoot } = args;
const { tasksJsonPath, id, projectRoot, tag } = args;
const { session } = context;
try {
// Check if tasksJsonPath was provided
@@ -56,17 +57,17 @@ export async function removeTaskDirect(args, log, context = {}) {
const taskIdArray = id.split(',').map((taskId) => taskId.trim());
log.info(
`Removing ${taskIdArray.length} task(s) with ID(s): ${taskIdArray.join(', ')} from ${tasksJsonPath}`
`Removing ${taskIdArray.length} task(s) with ID(s): ${taskIdArray.join(', ')} from ${tasksJsonPath}${tag ? ` in tag '${tag}'` : ''}`
);
// Validate all task IDs exist before proceeding
const data = readJSON(tasksJsonPath, projectRoot);
const data = readJSON(tasksJsonPath, projectRoot, tag);
if (!data || !data.tasks) {
return {
success: false,
error: {
code: 'INVALID_TASKS_FILE',
message: `No valid tasks found in ${tasksJsonPath}`
message: `No valid tasks found in ${tasksJsonPath}${tag ? ` for tag '${tag}'` : ''}`
}
};
}
@@ -80,71 +81,51 @@ export async function removeTaskDirect(args, log, context = {}) {
success: false,
error: {
code: 'INVALID_TASK_ID',
message: `The following tasks were not found: ${invalidTasks.join(', ')}`
message: `The following tasks were not found${tag ? ` in tag '${tag}'` : ''}: ${invalidTasks.join(', ')}`
}
};
}
// Remove tasks one by one
const results = [];
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
try {
for (const taskId of taskIdArray) {
try {
const result = await removeTask(tasksJsonPath, taskId);
results.push({
taskId,
success: true,
message: result.message,
removedTask: result.removedTask
});
log.info(`Successfully removed task: ${taskId}`);
} catch (error) {
results.push({
taskId,
success: false,
error: error.message
});
log.error(`Error removing task ${taskId}: ${error.message}`);
}
// Call removeTask with proper context including tag
const result = await removeTask(tasksJsonPath, id, {
projectRoot,
tag
});
if (!result.success) {
return {
success: false,
error: {
code: 'REMOVE_TASK_ERROR',
message: result.errors.join('; ') || 'Failed to remove tasks',
details: result.errors
}
};
}
log.info(`Successfully removed ${result.removedTasks.length} task(s)`);
return {
success: true,
data: {
totalTasks: taskIdArray.length,
successful: result.removedTasks.length,
failed: result.errors.length,
removedTasks: result.removedTasks,
messages: result.messages,
errors: result.errors,
tasksPath: tasksJsonPath,
tag: data.tag || tag || 'master'
}
};
} finally {
// Restore normal logging
disableSilentMode();
}
// Check if all tasks were successfully removed
const successfulRemovals = results.filter((r) => r.success);
const failedRemovals = results.filter((r) => !r.success);
if (successfulRemovals.length === 0) {
// All removals failed
return {
success: false,
error: {
code: 'REMOVE_TASK_ERROR',
message: 'Failed to remove any tasks',
details: failedRemovals
.map((r) => `${r.taskId}: ${r.error}`)
.join('; ')
}
};
}
// At least some tasks were removed successfully
return {
success: true,
data: {
totalTasks: taskIdArray.length,
successful: successfulRemovals.length,
failed: failedRemovals.length,
results: results,
tasksPath: tasksJsonPath
}
};
} catch (error) {
// Ensure silent mode is disabled even if an outer error occurs
disableSilentMode();

View File

@@ -20,7 +20,8 @@ import { nextTaskDirect } from './next-task.js';
*/
export async function setTaskStatusDirect(args, log, context = {}) {
// Destructure expected args, including the resolved tasksJsonPath and projectRoot
const { tasksJsonPath, id, status, complexityReportPath, projectRoot } = args;
const { tasksJsonPath, id, status, complexityReportPath, projectRoot, tag } =
args;
const { session } = context;
try {
log.info(`Setting task status with args: ${JSON.stringify(args)}`);
@@ -69,11 +70,17 @@ export async function setTaskStatusDirect(args, log, context = {}) {
enableSilentMode(); // Enable silent mode before calling core function
try {
// Call the core function
await setTaskStatus(tasksPath, taskId, newStatus, {
mcpLog: log,
projectRoot,
session
});
await setTaskStatus(
tasksPath,
taskId,
newStatus,
{
mcpLog: log,
projectRoot,
session
},
tag
);
log.info(`Successfully set task ${taskId} status to ${newStatus}`);

View File

@@ -52,6 +52,7 @@ export function registerAddSubtaskTool(server) {
.describe(
'Absolute path to the tasks file (default: tasks/tasks.json)'
),
tag: z.string().optional().describe('Tag context to operate on'),
skipGenerate: z
.boolean()
.optional()
@@ -89,7 +90,8 @@ export function registerAddSubtaskTool(server) {
status: args.status,
dependencies: args.dependencies,
skipGenerate: args.skipGenerate,
projectRoot: args.projectRoot
projectRoot: args.projectRoot,
tag: args.tag
},
log,
{ session }

View File

@@ -24,7 +24,8 @@ export function registerFixDependenciesTool(server) {
file: z.string().optional().describe('Absolute path to the tasks file'),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
.describe('The directory of the project. Must be an absolute path.'),
tag: z.string().optional().describe('Tag context to operate on')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
@@ -46,7 +47,9 @@ export function registerFixDependenciesTool(server) {
const result = await fixDependenciesDirect(
{
tasksJsonPath: tasksJsonPath
tasksJsonPath: tasksJsonPath,
projectRoot: args.projectRoot,
tag: args.tag
},
log
);

View File

@@ -33,7 +33,13 @@ export function registerRemoveTaskTool(server) {
confirm: z
.boolean()
.optional()
.describe('Whether to skip confirmation prompt (default: false)')
.describe('Whether to skip confirmation prompt (default: false)'),
tag: z
.string()
.optional()
.describe(
'Specify which tag context to operate on. Defaults to the current active tag.'
)
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
@@ -59,7 +65,8 @@ export function registerRemoveTaskTool(server) {
{
tasksJsonPath: tasksJsonPath,
id: args.id,
projectRoot: args.projectRoot
projectRoot: args.projectRoot,
tag: args.tag
},
log,
{ session }

View File

@@ -47,7 +47,8 @@ export function registerSetTaskStatusTool(server) {
),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
.describe('The directory of the project. Must be an absolute path.'),
tag: z.string().optional().describe('Optional tag context to operate on')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
@@ -86,7 +87,8 @@ export function registerSetTaskStatusTool(server) {
id: args.id,
status: args.status,
complexityReportPath,
projectRoot: args.projectRoot
projectRoot: args.projectRoot,
tag: args.tag
},
log,
{ session }