Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/olive-clouds-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---

Fix data corruption issues by ensuring project root and tag information is properly passed through all command operations
14 changes: 11 additions & 3 deletions mcp-server/src/core/direct-functions/add-subtask.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}>}
*/
Expand All @@ -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)}`);
Expand Down Expand Up @@ -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}`);
Expand All @@ -104,7 +110,8 @@ export async function addSubtaskDirect(args, log) {
parentId,
existingTaskId,
null,
generateFiles
generateFiles,
context
);

// Restore normal logging
Expand Down Expand Up @@ -135,7 +142,8 @@ export async function addSubtaskDirect(args, log) {
parentId,
null,
newSubtaskData,
generateFiles
generateFiles,
context
);

// Restore normal logging
Expand Down
4 changes: 2 additions & 2 deletions mcp-server/src/core/direct-functions/expand-task.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
13 changes: 9 additions & 4 deletions mcp-server/src/core/direct-functions/fix-dependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);

Expand Down Expand Up @@ -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();
Expand All @@ -61,7 +65,8 @@ export async function fixDependenciesDirect(args, log) {
success: true,
data: {
message: 'Dependencies fixed successfully',
tasksPath
tasksPath,
tag: tag || 'master'
}
};
} catch (error) {
Expand Down
89 changes: 35 additions & 54 deletions mcp-server/src/core/direct-functions/remove-task.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}'` : ''}`
}
};
}
Expand All @@ -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
}
};
}
} 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);
log.info(`Successfully removed ${result.removedTasks.length} task(s)`);

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('; ')
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();
}

// 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();
Expand Down
19 changes: 13 additions & 6 deletions mcp-server/src/core/direct-functions/set-task-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`);
Expand Down Expand Up @@ -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}`);

Expand Down
4 changes: 3 additions & 1 deletion mcp-server/src/tools/add-subtask.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 }
Expand Down
7 changes: 5 additions & 2 deletions mcp-server/src/tools/fix-dependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -46,7 +47,9 @@ export function registerFixDependenciesTool(server) {

const result = await fixDependenciesDirect(
{
tasksJsonPath: tasksJsonPath
tasksJsonPath: tasksJsonPath,
projectRoot: args.projectRoot,
tag: args.tag
},
log
);
Expand Down
11 changes: 9 additions & 2 deletions mcp-server/src/tools/remove-task.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -59,7 +65,8 @@ export function registerRemoveTaskTool(server) {
{
tasksJsonPath: tasksJsonPath,
id: args.id,
projectRoot: args.projectRoot
projectRoot: args.projectRoot,
tag: args.tag
},
log,
{ session }
Expand Down
6 changes: 4 additions & 2 deletions mcp-server/src/tools/set-task-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 }
Expand Down
10 changes: 6 additions & 4 deletions scripts/modules/task-manager/add-subtask.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'path';

import { log, readJSON, writeJSON } from '../utils.js';
import { log, readJSON, writeJSON, getCurrentTag } from '../utils.js';
import { isTaskDependentOn } from '../task-manager.js';
import generateTaskFiles from './generate-task-files.js';

Expand All @@ -25,8 +25,10 @@ async function addSubtask(
try {
log('info', `Adding subtask to parent task ${parentId}...`);

const currentTag =
context.tag || getCurrentTag(context.projectRoot) || 'master';
// Read the existing tasks with proper context
const data = readJSON(tasksPath, context.projectRoot, context.tag);
const data = readJSON(tasksPath, context.projectRoot, currentTag);
if (!data || !data.tasks) {
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
}
Expand Down Expand Up @@ -137,12 +139,12 @@ async function addSubtask(
}

// Write the updated tasks back to the file with proper context
writeJSON(tasksPath, data, context.projectRoot, context.tag);
writeJSON(tasksPath, data, context.projectRoot, currentTag);

// Generate task files if requested
if (generateFiles) {
log('info', 'Regenerating task files...');
// await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
}

return newSubtask;
Expand Down
Loading