update_subtask
Append timestamped updates to a specific subtask in the Task Master system. Add new information without replacing existing content, ensuring clear progress tracking. Requires subtask ID, project path, and update details.
Instructions
Appends timestamped information to a specific subtask without replacing existing content. If you just want to update the subtask status, use set_task_status instead.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| file | No | Absolute path to the tasks file | |
| id | Yes | ID of the subtask to update in format "parentId.subtaskId" (e.g., "5.2"). Parent ID is the ID of the task that contains the subtask. | |
| projectRoot | Yes | The directory of the project. Must be an absolute path. | |
| prompt | Yes | Information to add to the subtask | |
| research | No | Use Perplexity AI for research-backed updates | |
| tag | No | Tag context to operate on |
Implementation Reference
- MCP tool registration and handler for 'update_subtask'. Defines the tool schema, description, and execute function that handles input validation, finds tasks.json path, and delegates to the direct implementation via updateSubtaskByIdDirect.server.addTool({ name: 'update_subtask', description: 'Appends timestamped information to a specific subtask without replacing existing content. If you just want to update the subtask status, use set_task_status instead.', parameters: z.object({ id: z .string() .describe( 'ID of the subtask to update in format "parentId.subtaskId" (e.g., "5.2"). Parent ID is the ID of the task that contains the subtask.' ), prompt: z.string().describe('Information to add to the subtask'), research: z .boolean() .optional() .describe('Use Perplexity AI for research-backed updates'), 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.'), tag: z.string().optional().describe('Tag context to operate on') }), execute: withNormalizedProjectRoot(async (args, { log, session }) => { const toolName = 'update_subtask'; try { const resolvedTag = resolveTag({ projectRoot: args.projectRoot, tag: args.tag }); log.info(`Updating subtask with args: ${JSON.stringify(args)}`); let tasksJsonPath; try { tasksJsonPath = findTasksPath( { projectRoot: args.projectRoot, file: args.file }, log ); } catch (error) { log.error(`${toolName}: Error finding tasks.json: ${error.message}`); return createErrorResponse( `Failed to find tasks.json: ${error.message}` ); } const result = await updateSubtaskByIdDirect( { tasksJsonPath: tasksJsonPath, id: args.id, prompt: args.prompt, research: args.research, projectRoot: args.projectRoot, tag: resolvedTag }, log, { session } ); if (result.success) { log.info(`Successfully updated subtask with ID ${args.id}`); } else { log.error( `Failed to update subtask: ${result.error?.message || 'Unknown error'}` ); } return handleApiResult({ result, log: log, errorPrefix: 'Error updating subtask', projectRoot: args.projectRoot }); } catch (error) { log.error( `Critical error in ${toolName} tool execute: ${error.message}` ); return createErrorResponse( `Internal tool error (${toolName}): ${error.message}` ); } }) });
- src/schemas/update-subtask.js:1-7 (schema)Zod schema for the response of update_subtask tool, exporting UpdateSubtaskResponseSchema.import { z } from 'zod'; import { SubtaskSchema } from './base-schemas.js'; export const UpdateSubtaskResponseSchema = z.object({ subtask: SubtaskSchema });
- mcp-server/src/tools/tool-registry.js:33-79 (registration)Central tool registry mapping 'update_subtask' to its registration function registerUpdateSubtaskTool.import { registerUpdateSubtaskTool } from './update-subtask.js'; import { registerUpdateTaskTool } from './update-task.js'; import { registerUpdateTool } from './update.js'; import { registerUseTagTool } from './use-tag.js'; import { registerValidateDependenciesTool } from './validate-dependencies.js'; // Import TypeScript tools from apps/mcp import { registerAutopilotAbortTool, registerAutopilotCommitTool, registerAutopilotCompleteTool, registerAutopilotFinalizeTool, registerAutopilotNextTool, registerAutopilotResumeTool, registerAutopilotStartTool, registerAutopilotStatusTool, registerGenerateTool, registerGetTaskTool, registerGetTasksTool, registerSetTaskStatusTool } from '@tm/mcp'; /** * Comprehensive tool registry mapping tool names to their registration functions * Used for dynamic tool registration and validation */ export const toolRegistry = { initialize_project: registerInitializeProjectTool, models: registerModelsTool, rules: registerRulesTool, parse_prd: registerParsePRDTool, 'response-language': registerResponseLanguageTool, analyze_project_complexity: registerAnalyzeProjectComplexityTool, expand_task: registerExpandTaskTool, expand_all: registerExpandAllTool, scope_up_task: registerScopeUpTool, scope_down_task: registerScopeDownTool, get_tasks: registerGetTasksTool, get_task: registerGetTaskTool, next_task: registerNextTaskTool, complexity_report: registerComplexityReportTool, set_task_status: registerSetTaskStatusTool, add_task: registerAddTaskTool, add_subtask: registerAddSubtaskTool, update: registerUpdateTool, update_task: registerUpdateTaskTool, update_subtask: registerUpdateSubtaskTool,
- Direct function implementation that wraps the core updateSubtaskById logic, adds MCP-specific validation, logging, and calls the legacy script function.export async function updateSubtaskByIdDirect(args, log, context = {}) { const { session } = context; // Destructure expected args, including projectRoot const { tasksJsonPath, id, prompt, research, projectRoot, tag } = args; const logWrapper = createLogWrapper(log); try { logWrapper.info( `Updating subtask by ID via direct function. ID: ${id}, ProjectRoot: ${projectRoot}` ); // Check if tasksJsonPath was provided if (!tasksJsonPath) { const errorMessage = 'tasksJsonPath is required but was not provided.'; logWrapper.error(errorMessage); return { success: false, error: { code: 'MISSING_ARGUMENT', message: errorMessage } }; } // Basic validation for ID format (e.g., '5.2') if (!id || typeof id !== 'string' || !id.includes('.')) { const errorMessage = 'Invalid subtask ID format. Must be in format "parentId.subtaskId" (e.g., "5.2").'; logWrapper.error(errorMessage); return { success: false, error: { code: 'INVALID_SUBTASK_ID', message: errorMessage } }; } if (!prompt) { const errorMessage = 'No prompt specified. Please provide the information to append.'; logWrapper.error(errorMessage); return { success: false, error: { code: 'MISSING_PROMPT', message: errorMessage } }; } // Validate subtask ID format const subtaskId = id; if (typeof subtaskId !== 'string' && typeof subtaskId !== 'number') { const errorMessage = `Invalid subtask ID type: ${typeof subtaskId}. Subtask ID must be a string or number.`; log.error(errorMessage); return { success: false, error: { code: 'INVALID_SUBTASK_ID_TYPE', message: errorMessage } }; } const subtaskIdStr = String(subtaskId); if (!subtaskIdStr.includes('.')) { const errorMessage = `Invalid subtask ID format: ${subtaskIdStr}. Subtask ID must be in format "parentId.subtaskId" (e.g., "5.2").`; log.error(errorMessage); return { success: false, error: { code: 'INVALID_SUBTASK_ID_FORMAT', message: errorMessage } }; } // Use the provided path const tasksPath = tasksJsonPath; const useResearch = research === true; log.info( `Updating subtask with ID ${subtaskIdStr} with prompt "${prompt}" and research: ${useResearch}` ); const wasSilent = isSilentMode(); if (!wasSilent) { enableSilentMode(); } try { // Call legacy script which handles both API and file storage via bridge const coreResult = await updateSubtaskById( tasksPath, subtaskIdStr, prompt, useResearch, { mcpLog: logWrapper, session, projectRoot, tag, commandName: 'update-subtask', outputType: 'mcp' }, 'json' ); if (!coreResult || coreResult.updatedSubtask === null) { const message = `Subtask ${id} or its parent task not found.`; logWrapper.error(message); return { success: false, error: { code: 'SUBTASK_NOT_FOUND', message: message } }; } const parentId = subtaskIdStr.split('.')[0]; const successMessage = `Successfully updated subtask with ID ${subtaskIdStr}`; logWrapper.success(successMessage); return { success: true, data: { message: `Successfully updated subtask with ID ${subtaskIdStr}`, subtaskId: subtaskIdStr, parentId: parentId, subtask: coreResult.updatedSubtask, tasksPath, useResearch, telemetryData: coreResult.telemetryData, tagInfo: coreResult.tagInfo } }; } catch (error) { logWrapper.error(`Error updating subtask by ID: ${error.message}`); return { success: false, error: { code: 'UPDATE_SUBTASK_CORE_ERROR', message: error.message || 'Unknown error updating subtask' } }; } finally { if (!wasSilent && isSilentMode()) { disableSilentMode(); } } } catch (error) { logWrapper.error( `Setup error in updateSubtaskByIdDirect: ${error.message}` ); if (isSilentMode()) disableSilentMode(); return { success: false, error: { code: 'DIRECT_FUNCTION_SETUP_ERROR', message: error.message || 'Unknown setup error' } }; } }
- Core legacy implementation of subtask update logic: file I/O, AI content generation, timestamped append to subtask details.async function updateSubtaskById( tasksPath, subtaskId, prompt, useResearch = false, context = {}, outputFormat = context.mcpLog ? 'json' : 'text' ) { const { session, mcpLog, projectRoot: providedProjectRoot, tag } = context; const logFn = mcpLog || consoleLog; const isMCP = !!mcpLog; // Report helper const report = (level, ...args) => { if (isMCP) { if (typeof logFn[level] === 'function') logFn[level](...args); else logFn.info(...args); } else if (!isSilentMode()) { logFn(level, ...args); } }; let loadingIndicator = null; try { report('info', `Updating subtask ${subtaskId} with prompt: "${prompt}"`); if ( !subtaskId || typeof subtaskId !== 'string' || !subtaskId.includes('.') ) { throw new Error( `Invalid subtask ID format: ${subtaskId}. Subtask ID must be in format "parentId.subtaskId"` ); } if (!prompt || typeof prompt !== 'string' || prompt.trim() === '') { throw new Error( 'Prompt cannot be empty. Please provide context for the subtask update.' ); } if (!fs.existsSync(tasksPath)) { throw new Error(`Tasks file not found at path: ${tasksPath}`); } const projectRoot = providedProjectRoot || findProjectRoot(); if (!projectRoot) { throw new Error('Could not determine project root directory'); } // --- BRIDGE: Try remote update first (API storage) --- // In API storage, subtask IDs like "1.2" or "TAS-49.1" are just regular task IDs // So update-subtask and update-task work identically const remoteResult = await tryUpdateViaRemote({ taskId: subtaskId, prompt, projectRoot, tag, appendMode: true, // Subtask updates are always append mode useResearch, isMCP, outputFormat, report }); // If remote handled it, return the result if (remoteResult) { return { updatedSubtask: { id: subtaskId }, telemetryData: remoteResult.telemetryData, tagInfo: remoteResult.tagInfo }; } // Otherwise fall through to file-based logic below // --- End BRIDGE --- const data = readJSON(tasksPath, projectRoot, tag); if (!data || !data.tasks) { throw new Error( `No valid tasks found in ${tasksPath}. The file may be corrupted or have an invalid format.` ); } const [parentIdStr, subtaskIdStr] = subtaskId.split('.'); const parentId = parseInt(parentIdStr, 10); const subtaskIdNum = parseInt(subtaskIdStr, 10); if ( Number.isNaN(parentId) || parentId <= 0 || Number.isNaN(subtaskIdNum) || subtaskIdNum <= 0 ) { throw new Error( `Invalid subtask ID format: ${subtaskId}. Both parent ID and subtask ID must be positive integers.` ); } const parentTask = data.tasks.find((task) => task.id === parentId); if (!parentTask) { throw new Error( `Parent task with ID ${parentId} not found. Please verify the task ID and try again.` ); } if (!parentTask.subtasks || !Array.isArray(parentTask.subtasks)) { throw new Error(`Parent task ${parentId} has no subtasks.`); } const subtaskIndex = parentTask.subtasks.findIndex( (st) => st.id === subtaskIdNum ); if (subtaskIndex === -1) { throw new Error( `Subtask with ID ${subtaskId} not found. Please verify the subtask ID and try again.` ); } const subtask = parentTask.subtasks[subtaskIndex]; // --- Context Gathering --- let gatheredContext = ''; try { const contextGatherer = new ContextGatherer(projectRoot, tag); const allTasksFlat = flattenTasksWithSubtasks(data.tasks); const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update-subtask'); const searchQuery = `${parentTask.title} ${subtask.title} ${prompt}`; const searchResults = fuzzySearch.findRelevantTasks(searchQuery, { maxResults: 5, includeSelf: true }); const relevantTaskIds = fuzzySearch.getTaskIds(searchResults); const finalTaskIds = [ ...new Set([subtaskId.toString(), ...relevantTaskIds]) ]; if (finalTaskIds.length > 0) { const contextResult = await contextGatherer.gather({ tasks: finalTaskIds, format: 'research' }); gatheredContext = contextResult.context || ''; } } catch (contextError) { report('warn', `Could not gather context: ${contextError.message}`); } // --- End Context Gathering --- if (outputFormat === 'text') { const table = new Table({ head: [ chalk.cyan.bold('ID'), chalk.cyan.bold('Title'), chalk.cyan.bold('Status') ], colWidths: [10, 55, 10] }); table.push([ subtaskId, truncate(subtask.title, 52), getStatusWithColor(subtask.status) ]); console.log( boxen(chalk.white.bold(`Updating Subtask #${subtaskId}`), { padding: 1, borderColor: 'blue', borderStyle: 'round', margin: { top: 1, bottom: 0 } }) ); console.log(table.toString()); loadingIndicator = startLoadingIndicator( useResearch ? 'Updating subtask with research...' : 'Updating subtask...' ); } let generatedContentString = ''; let newlyAddedSnippet = ''; let aiServiceResponse = null; try { const parentContext = { id: parentTask.id, title: parentTask.title }; const prevSubtask = subtaskIndex > 0 ? { id: `${parentTask.id}.${parentTask.subtasks[subtaskIndex - 1].id}`, title: parentTask.subtasks[subtaskIndex - 1].title, status: parentTask.subtasks[subtaskIndex - 1].status } : undefined; const nextSubtask = subtaskIndex < parentTask.subtasks.length - 1 ? { id: `${parentTask.id}.${parentTask.subtasks[subtaskIndex + 1].id}`, title: parentTask.subtasks[subtaskIndex + 1].title, status: parentTask.subtasks[subtaskIndex + 1].status } : undefined; // Build prompts using PromptManager const promptManager = getPromptManager(); const promptParams = { parentTask: parentContext, prevSubtask: prevSubtask, nextSubtask: nextSubtask, currentDetails: subtask.details || '(No existing details)', updatePrompt: prompt, useResearch: useResearch, gatheredContext: gatheredContext || '', hasCodebaseAnalysis: hasCodebaseAnalysis( useResearch, projectRoot, session ), projectRoot: projectRoot }; const variantKey = useResearch ? 'research' : 'default'; const { systemPrompt, userPrompt } = await promptManager.loadPrompt( 'update-subtask', promptParams, variantKey ); const role = useResearch ? 'research' : 'main'; report('info', `Using AI text service with role: ${role}`); aiServiceResponse = await generateTextService({ prompt: userPrompt, systemPrompt: systemPrompt, role, session, projectRoot, maxRetries: 2, commandName: 'update-subtask', outputType: isMCP ? 'mcp' : 'cli' }); if ( aiServiceResponse && aiServiceResponse.mainResult && typeof aiServiceResponse.mainResult === 'string' ) { generatedContentString = aiServiceResponse.mainResult; } else { generatedContentString = ''; report( 'warn', 'AI service response did not contain expected text string.' ); } if (outputFormat === 'text' && loadingIndicator) { stopLoadingIndicator(loadingIndicator); loadingIndicator = null; } } catch (aiError) { report('error', `AI service call failed: ${aiError.message}`); if (outputFormat === 'text' && loadingIndicator) { stopLoadingIndicator(loadingIndicator); loadingIndicator = null; } throw aiError; } if (generatedContentString && generatedContentString.trim()) { // Check if the string is not empty const timestamp = new Date().toISOString(); const formattedBlock = `<info added on ${timestamp}>\n${generatedContentString.trim()}\n</info added on ${timestamp}>`; newlyAddedSnippet = formattedBlock; // <--- ADD THIS LINE: Store for display subtask.details = (subtask.details ? subtask.details + '\n' : '') + formattedBlock; } else { report( 'warn', 'AI response was empty or whitespace after trimming. Original details remain unchanged.' ); newlyAddedSnippet = 'No new details were added by the AI.'; } const updatedSubtask = parentTask.subtasks[subtaskIndex]; if (outputFormat === 'text' && getDebugFlag(session)) { console.log( '>>> DEBUG: Subtask details AFTER AI update:', updatedSubtask.details ); } if (updatedSubtask.description) { if (prompt.length < 100) { if (outputFormat === 'text' && getDebugFlag(session)) { console.log( '>>> DEBUG: Subtask description BEFORE append:', updatedSubtask.description ); } updatedSubtask.description += ` [Updated: ${new Date().toLocaleDateString()}]`; if (outputFormat === 'text' && getDebugFlag(session)) { console.log( '>>> DEBUG: Subtask description AFTER append:', updatedSubtask.description ); } } } if (outputFormat === 'text' && getDebugFlag(session)) { console.log('>>> DEBUG: About to call writeJSON with updated data...'); } writeJSON(tasksPath, data, projectRoot, tag); if (outputFormat === 'text' && getDebugFlag(session)) { console.log('>>> DEBUG: writeJSON call completed.'); } report('success', `Successfully updated subtask ${subtaskId}`); // Updated function call to make sure if uncommented it will generate the task files for the updated subtask based on the tag // await generateTaskFiles(tasksPath, path.dirname(tasksPath), { // tag: tag, // projectRoot: projectRoot // }); if (outputFormat === 'text') { if (loadingIndicator) { stopLoadingIndicator(loadingIndicator); loadingIndicator = null; } console.log( boxen( chalk.green(`Successfully updated subtask #${subtaskId}`) + '\n\n' + chalk.white.bold('Title:') + ' ' + updatedSubtask.title + '\n\n' + chalk.white.bold('Newly Added Snippet:') + '\n' + chalk.white(newlyAddedSnippet), { padding: 1, borderColor: 'green', borderStyle: 'round' } ) ); } if (outputFormat === 'text' && aiServiceResponse.telemetryData) { displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli'); } return { updatedSubtask: updatedSubtask, telemetryData: aiServiceResponse.telemetryData, tagInfo: aiServiceResponse.tagInfo }; } catch (error) { if (outputFormat === 'text' && loadingIndicator) { stopLoadingIndicator(loadingIndicator); loadingIndicator = null; } report('error', `Error updating subtask: ${error.message}`); if (outputFormat === 'text') { console.error(chalk.red(`Error: ${error.message}`)); if (error.message?.includes('ANTHROPIC_API_KEY')) { console.log( chalk.yellow('\nTo fix this issue, set your Anthropic API key:') ); console.log(' export ANTHROPIC_API_KEY=your_api_key_here'); } else if (error.message?.includes('PERPLEXITY_API_KEY')) { console.log(chalk.yellow('\nTo fix this issue:')); console.log( ' 1. Set your Perplexity API key: export PERPLEXITY_API_KEY=your_api_key_here' ); console.log( ' 2. Or run without the research flag: task-master update-subtask --id=<id> --prompt="..."' ); } else if (error.message?.includes('overloaded')) { console.log( chalk.yellow( '\nAI model overloaded, and fallback failed or was unavailable:' ) ); console.log(' 1. Try again in a few minutes.'); console.log(' 2. Ensure PERPLEXITY_API_KEY is set for fallback.'); } else if (error.message?.includes('not found')) { console.log(chalk.yellow('\nTo fix this issue:')); console.log( ' 1. Run task-master list --with-subtasks to see all available subtask IDs' ); console.log( ' 2. Use a valid subtask ID with the --id parameter in format "parentId.subtaskId"' ); } else if ( error.message?.includes('empty stream response') || error.message?.includes('AI did not return a valid text string') ) { console.log( chalk.yellow( '\nThe AI model returned an empty or invalid response. This might be due to the prompt or API issues. Try rephrasing or trying again later.' ) ); } if (getDebugFlag(session)) { console.error(error); } } else { throw error; } return null; } }