set_task_status
Update the status of tasks or subtasks (e.g., pending, done, in-progress) by specifying task IDs, status, and project directory. Ideal for task management in AI-driven development workflows.
Instructions
Set the status of one or more tasks or subtasks.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| complexityReport | No | Path to the complexity report file (relative to project root or absolute) | |
| file | No | Absolute path to the tasks file | |
| id | Yes | Task ID or subtask ID (e.g., '15', '15.2'). Can be comma-separated to update multiple tasks/subtasks at once. | |
| projectRoot | Yes | The directory of the project. Must be an absolute path. | |
| status | Yes | New status to set (e.g., 'pending', 'done', 'in-progress', 'review', 'deferred', 'cancelled'. | |
| tag | No | Optional tag context to operate on |
Implementation Reference
- MCP tool execute handler for 'set_task_status'. Normalizes project root, resolves paths to tasks.json and complexity report, calls setTaskStatusDirect, and handles the result.execute: withNormalizedProjectRoot(async (args, { log, session }) => { try { log.info( `Setting status of task(s) ${args.id} to: ${args.status} ${ args.tag ? `in tag: ${args.tag}` : 'in current tag' }` ); const resolvedTag = resolveTag({ projectRoot: args.projectRoot, tag: args.tag }); // Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot) let tasksJsonPath; try { tasksJsonPath = findTasksPath( { projectRoot: args.projectRoot, file: args.file }, log ); } catch (error) { log.error(`Error finding tasks.json: ${error.message}`); return createErrorResponse( `Failed to find tasks.json: ${error.message}` ); } let complexityReportPath; try { complexityReportPath = findComplexityReportPath( { projectRoot: args.projectRoot, complexityReport: args.complexityReport, tag: resolvedTag }, log ); } catch (error) { log.error(`Error finding complexity report: ${error.message}`); } const result = await setTaskStatusDirect( { tasksJsonPath: tasksJsonPath, id: args.id, status: args.status, complexityReportPath, projectRoot: args.projectRoot, tag: resolvedTag }, log, { session } ); if (result.success) { log.info( `Successfully updated status for task(s) ${args.id} to "${args.status}": ${result.data.message}` ); } else { log.error( `Failed to update task status: ${result.error?.message || 'Unknown error'}` ); } return handleApiResult({ result, log: log, errorPrefix: 'Error setting task status', projectRoot: args.projectRoot }); } catch (error) { log.error(`Error in setTaskStatus tool: ${error.message}`); return createErrorResponse( `Error setting task status: ${error.message}` ); } }) });
- Zod schema defining input parameters for the set_task_status tool: id, status, file, complexityReport, projectRoot, tag.parameters: z.object({ id: z .string() .describe( "Task ID or subtask ID (e.g., '15', '15.2'). Can be comma-separated to update multiple tasks/subtasks at once." ), status: z .enum(TASK_STATUS_OPTIONS) .describe( "New status to set (e.g., 'pending', 'done', 'in-progress', 'review', 'deferred', 'cancelled'." ), file: z.string().optional().describe('Absolute path to the tasks file'), complexityReport: z .string() .optional() .describe( 'Path to the complexity report file (relative to project root or absolute)' ), projectRoot: z .string() .describe('The directory of the project. Must be an absolute path.'), tag: z.string().optional().describe('Optional tag context to operate on') }),
- mcp-server/src/tools/set-task-status.js:27-130 (registration)Registers the 'set_task_status' tool on the MCP server instance using server.addTool, specifying name, description, parameters, and execute function.export function registerSetTaskStatusTool(server) { server.addTool({ name: 'set_task_status', description: 'Set the status of one or more tasks or subtasks.', parameters: z.object({ id: z .string() .describe( "Task ID or subtask ID (e.g., '15', '15.2'). Can be comma-separated to update multiple tasks/subtasks at once." ), status: z .enum(TASK_STATUS_OPTIONS) .describe( "New status to set (e.g., 'pending', 'done', 'in-progress', 'review', 'deferred', 'cancelled'." ), file: z.string().optional().describe('Absolute path to the tasks file'), complexityReport: z .string() .optional() .describe( 'Path to the complexity report file (relative to project root or absolute)' ), projectRoot: z .string() .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 { log.info( `Setting status of task(s) ${args.id} to: ${args.status} ${ args.tag ? `in tag: ${args.tag}` : 'in current tag' }` ); const resolvedTag = resolveTag({ projectRoot: args.projectRoot, tag: args.tag }); // Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot) let tasksJsonPath; try { tasksJsonPath = findTasksPath( { projectRoot: args.projectRoot, file: args.file }, log ); } catch (error) { log.error(`Error finding tasks.json: ${error.message}`); return createErrorResponse( `Failed to find tasks.json: ${error.message}` ); } let complexityReportPath; try { complexityReportPath = findComplexityReportPath( { projectRoot: args.projectRoot, complexityReport: args.complexityReport, tag: resolvedTag }, log ); } catch (error) { log.error(`Error finding complexity report: ${error.message}`); } const result = await setTaskStatusDirect( { tasksJsonPath: tasksJsonPath, id: args.id, status: args.status, complexityReportPath, projectRoot: args.projectRoot, tag: resolvedTag }, log, { session } ); if (result.success) { log.info( `Successfully updated status for task(s) ${args.id} to "${args.status}": ${result.data.message}` ); } else { log.error( `Failed to update task status: ${result.error?.message || 'Unknown error'}` ); } return handleApiResult({ result, log: log, errorPrefix: 'Error setting task status', projectRoot: args.projectRoot }); } catch (error) { log.error(`Error in setTaskStatus tool: ${error.message}`); return createErrorResponse( `Error setting task status: ${error.message}` ); } }) }); }
- Direct function setTaskStatusDirect: validates args, handles silent mode, calls core setTaskStatus, optionally fetches next task if done, and returns structured result.export async function setTaskStatusDirect(args, log, context = {}) { // Destructure expected args, including the resolved tasksJsonPath and projectRoot const { tasksJsonPath, id, status, complexityReportPath, projectRoot, tag } = args; const { session } = context; try { log.info(`Setting task status with args: ${JSON.stringify(args)}`); // Check if tasksJsonPath was provided if (!tasksJsonPath) { const errorMessage = 'tasksJsonPath is required but was not provided.'; log.error(errorMessage); return { success: false, error: { code: 'MISSING_ARGUMENT', message: errorMessage } }; } // Check required parameters (id and status) if (!id) { const errorMessage = 'No task ID specified. Please provide a task ID to update.'; log.error(errorMessage); return { success: false, error: { code: 'MISSING_TASK_ID', message: errorMessage } }; } if (!status) { const errorMessage = 'No status specified. Please provide a new status value.'; log.error(errorMessage); return { success: false, error: { code: 'MISSING_STATUS', message: errorMessage } }; } // Use the provided path const tasksPath = tasksJsonPath; // Execute core setTaskStatus function const taskId = id; const newStatus = status; log.info(`Setting task ${taskId} status to "${newStatus}"`); // Call the core function with proper silent mode handling enableSilentMode(); // Enable silent mode before calling core function try { // Call the core function await setTaskStatus(tasksPath, taskId, newStatus, { mcpLog: log, projectRoot, session, tag }); log.info(`Successfully set task ${taskId} status to ${newStatus}`); // Return success data const result = { success: true, data: { message: `Successfully updated task ${taskId} status to "${newStatus}"`, taskId, status: newStatus, tasksPath: tasksPath // Return the path used } }; // If the task was completed, attempt to fetch the next task if (result.data.status === 'done') { try { log.info(`Attempting to fetch next task for task ${taskId}`); const nextResult = await nextTaskDirect( { tasksJsonPath: tasksJsonPath, reportPath: complexityReportPath, projectRoot: projectRoot, tag }, log, { session } ); if (nextResult.success) { log.info( `Successfully retrieved next task: ${nextResult.data.nextTask}` ); result.data = { ...result.data, nextTask: nextResult.data.nextTask, isNextSubtask: nextResult.data.isSubtask, nextSteps: nextResult.data.nextSteps }; } else { log.warn( `Failed to retrieve next task: ${nextResult.error?.message || 'Unknown error'}` ); } } catch (nextErr) { log.error(`Error retrieving next task: ${nextErr.message}`); } } return result; } catch (error) { log.error(`Error setting task status: ${error.message}`); return { success: false, error: { code: 'SET_STATUS_ERROR', message: error.message || 'Unknown error setting task status' } }; } finally { // ALWAYS restore normal logging in finally block disableSilentMode(); } } catch (error) { // Ensure silent mode is disabled if there was an uncaught error in the outer try block if (isSilentMode()) { disableSilentMode(); } log.error(`Error setting task status: ${error.message}`); return { success: false, error: { code: 'SET_STATUS_ERROR', message: error.message || 'Unknown error setting task status' } }; } }
- Core helper function setTaskStatus: reads tasks.json, updates status for multiple task IDs (including subtasks), writes back, validates dependencies. Used by MCP direct function.async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) { const { projectRoot, tag } = options; try { if (!isValidTaskStatus(newStatus)) { throw new Error( `Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(', ')}` ); } // Determine if we're in MCP mode by checking for mcpLog const isMcpMode = !!options?.mcpLog; // Only display UI elements if not in MCP mode if (!isMcpMode) { console.log( boxen(chalk.white.bold(`Updating Task Status to: ${newStatus}`), { padding: 1, borderColor: 'blue', borderStyle: 'round' }) ); } log('info', `Reading tasks from ${tasksPath}...`); // Read the raw data without tag resolution to preserve tagged structure let rawData = readJSON(tasksPath, projectRoot, tag); // No tag parameter // Handle the case where readJSON returns resolved data with _rawTaggedData if (rawData && rawData._rawTaggedData) { // Use the raw tagged data and discard the resolved view rawData = rawData._rawTaggedData; } // Ensure the tag exists in the raw data if (!rawData || !rawData[tag] || !Array.isArray(rawData[tag].tasks)) { throw new Error( `Invalid tasks file or tag "${tag}" not found at ${tasksPath}` ); } // Get the tasks for the current tag const data = { tasks: rawData[tag].tasks, tag, _rawTaggedData: rawData }; if (!data || !data.tasks) { throw new Error(`No valid tasks found in ${tasksPath}`); } // Handle multiple task IDs (comma-separated) const taskIds = taskIdInput.split(',').map((id) => id.trim()); const updatedTasks = []; // Update each task and capture old status for display for (const id of taskIds) { // Capture old status before updating let oldStatus = 'unknown'; if (id.includes('.')) { // Handle subtask const [parentId, subtaskId] = id .split('.') .map((id) => parseInt(id, 10)); const parentTask = data.tasks.find((t) => t.id === parentId); if (parentTask?.subtasks) { const subtask = parentTask.subtasks.find((st) => st.id === subtaskId); oldStatus = subtask?.status || 'pending'; } } else { // Handle regular task const taskId = parseInt(id, 10); const task = data.tasks.find((t) => t.id === taskId); oldStatus = task?.status || 'pending'; } await updateSingleTaskStatus(tasksPath, id, newStatus, data, !isMcpMode); updatedTasks.push({ id, oldStatus, newStatus }); } // Update the raw data structure with the modified tasks rawData[tag].tasks = data.tasks; // Ensure the tag has proper metadata ensureTagMetadata(rawData[tag], { description: `Tasks for ${tag} context` }); // Write the updated raw data back to the file // The writeJSON function will automatically filter out _rawTaggedData writeJSON(tasksPath, rawData, projectRoot, tag); // Validate dependencies after status update log('info', 'Validating dependencies after status update...'); validateTaskDependencies(data.tasks); // Generate individual task files // log('info', 'Regenerating task files...'); // await generateTaskFiles(tasksPath, path.dirname(tasksPath), { // mcpLog: options.mcpLog // }); // Display success message - only in CLI mode if (!isMcpMode) { for (const updateInfo of updatedTasks) { const { id, oldStatus, newStatus: updatedStatus } = updateInfo; console.log( boxen( chalk.white.bold(`Successfully updated task ${id} status:`) + '\n' + `From: ${chalk.yellow(oldStatus)}\n` + `To: ${chalk.green(updatedStatus)}`, { padding: 1, borderColor: 'green', borderStyle: 'round' } ) ); } } // Return success value for programmatic use return { success: true, updatedTasks: updatedTasks.map(({ id, oldStatus, newStatus }) => ({ id, oldStatus, newStatus })) }; } catch (error) { log('error', `Error setting task status: ${error.message}`); // Only show error UI in CLI mode if (!options?.mcpLog) { console.error(chalk.red(`Error: ${error.message}`)); // Pass session to getDebugFlag if (getDebugFlag(options?.session)) { // Use getter console.error(error); } process.exit(1); } else { // In MCP mode, throw the error for the caller to handle throw error; } } } export default setTaskStatus;