expand_task
Break down complex tasks into manageable subtasks for detailed implementation using the Task Master MCP server. Specify task ID, project root, and optional parameters like subtask count or research context to streamline task execution.
Instructions
Expand a task into subtasks for detailed implementation
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| file | No | Path to the tasks file relative to project root (e.g., tasks/tasks.json) | |
| force | No | Force expansion even if subtasks exist | |
| id | Yes | ID of task to expand | |
| num | No | Number of subtasks to generate | |
| projectRoot | Yes | The directory of the project. Must be an absolute path. | |
| prompt | No | Additional context for subtask generation | |
| research | No | Use research role for generation | |
| tag | No | Tag context to operate on |
Implementation Reference
- mcp-server/src/tools/expand-task.js:23-109 (registration)Registers the MCP tool 'expand_task' with input schema, description, and execute handler that normalizes project root, finds tasks path, and calls expandTaskDirect.export function registerExpandTaskTool(server) { server.addTool({ name: 'expand_task', description: 'Expand a task into subtasks for detailed implementation', parameters: z.object({ id: z.string().describe('ID of task to expand'), num: z.string().optional().describe('Number of subtasks to generate'), research: z .boolean() .optional() .default(false) .describe('Use research role for generation'), prompt: z .string() .optional() .describe('Additional context for subtask generation'), file: z .string() .optional() .describe( 'Path to the tasks file relative to project root (e.g., tasks/tasks.json)' ), projectRoot: z .string() .describe('The directory of the project. Must be an absolute path.'), force: z .boolean() .optional() .default(false) .describe('Force expansion even if subtasks exist'), tag: z.string().optional().describe('Tag context to operate on') }), execute: withNormalizedProjectRoot(async (args, { log, session }) => { try { log.info(`Starting expand-task with args: ${JSON.stringify(args)}`); 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}` ); } const complexityReportPath = findComplexityReportPath( { ...args, tag: resolvedTag }, log ); const result = await expandTaskDirect( { tasksJsonPath: tasksJsonPath, id: args.id, num: args.num, research: args.research, prompt: args.prompt, force: args.force, complexityReportPath, projectRoot: args.projectRoot, tag: resolvedTag }, log, { session } ); return handleApiResult({ result, log: log, errorPrefix: 'Error expanding task', projectRoot: args.projectRoot }); } catch (error) { log.error(`Error in expand-task tool: ${error.message}`); return createErrorResponse(error.message); } }) }); }
- Direct handler function expandTaskDirect called by the tool execute; handles MCP session, file I/O, task validation, and delegates to core expandTask implementation.export async function expandTaskDirect(args, log, context = {}) { const { session } = context; // Extract session // Destructure expected args, including projectRoot const { tasksJsonPath, id, num, research, prompt, force, projectRoot, tag, complexityReportPath } = args; // Log session root data for debugging log.info( `Session data in expandTaskDirect: ${JSON.stringify({ hasSession: !!session, sessionKeys: session ? Object.keys(session) : [], roots: session?.roots, rootsStr: JSON.stringify(session?.roots) })}` ); // Check if tasksJsonPath was provided if (!tasksJsonPath) { log.error('expandTaskDirect called without tasksJsonPath'); return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'tasksJsonPath is required' } }; } // Use provided path const tasksPath = tasksJsonPath; log.info(`[expandTaskDirect] Using tasksPath: ${tasksPath}`); // Validate task ID const taskId = id ? parseInt(id, 10) : null; if (!taskId) { log.error('Task ID is required'); return { success: false, error: { code: 'INPUT_VALIDATION_ERROR', message: 'Task ID is required' } }; } // Process other parameters const numSubtasks = num ? parseInt(num, 10) : undefined; const useResearch = research === true; const additionalContext = prompt || ''; const forceFlag = force === true; try { log.info( `[expandTaskDirect] Expanding task ${taskId} into ${numSubtasks || 'default'} subtasks. Research: ${useResearch}, Force: ${forceFlag}` ); // Read tasks data log.info(`[expandTaskDirect] Attempting to read JSON from: ${tasksPath}`); const data = readJSON(tasksPath, projectRoot); log.info( `[expandTaskDirect] Result of readJSON: ${data ? 'Data read successfully' : 'readJSON returned null or undefined'}` ); if (!data || !data.tasks) { log.error( `[expandTaskDirect] readJSON failed or returned invalid data for path: ${tasksPath}` ); return { success: false, error: { code: 'INVALID_TASKS_FILE', message: `No valid tasks found in ${tasksPath}. readJSON returned: ${JSON.stringify(data)}` } }; } // Find the specific task log.info(`[expandTaskDirect] Searching for task ID ${taskId} in data`); const task = data.tasks.find((t) => t.id === taskId); log.info(`[expandTaskDirect] Task found: ${task ? 'Yes' : 'No'}`); if (!task) { return { success: false, error: { code: 'TASK_NOT_FOUND', message: `Task with ID ${taskId} not found` } }; } // Check if task is completed if (task.status === 'done' || task.status === 'completed') { return { success: false, error: { code: 'TASK_COMPLETED', message: `Task ${taskId} is already marked as ${task.status} and cannot be expanded` } }; } // Check for existing subtasks and force flag const hasExistingSubtasks = task.subtasks && task.subtasks.length > 0; if (hasExistingSubtasks && !forceFlag) { log.info( `Task ${taskId} already has ${task.subtasks.length} subtasks. Use --force to overwrite.` ); return { success: true, data: { message: `Task ${taskId} already has subtasks. Expansion skipped.`, task, subtasksAdded: 0, hasExistingSubtasks } }; } // If force flag is set, clear existing subtasks if (hasExistingSubtasks && forceFlag) { log.info( `Force flag set. Clearing existing subtasks for task ${taskId}.` ); task.subtasks = []; } // Keep a copy of the task before modification const originalTask = JSON.parse(JSON.stringify(task)); // Tracking subtasks count before expansion const subtasksCountBefore = task.subtasks ? task.subtasks.length : 0; // Directly modify the data instead of calling the CLI function if (!task.subtasks) { task.subtasks = []; } // 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); let wasSilent; // Declare wasSilent outside the try block // Process the request try { // Enable silent mode to prevent console logs from interfering with JSON response wasSilent = isSilentMode(); // Assign inside the try block if (!wasSilent) enableSilentMode(); // Call the core expandTask function with the wrapped logger and projectRoot const coreResult = await expandTask( tasksPath, taskId, numSubtasks, useResearch, additionalContext, { complexityReportPath, mcpLog, session, projectRoot, commandName: 'expand-task', outputType: 'mcp', tag }, forceFlag ); // Restore normal logging if (!wasSilent && isSilentMode()) disableSilentMode(); // Read the updated data const updatedData = readJSON(tasksPath, projectRoot); const updatedTask = updatedData.tasks.find((t) => t.id === taskId); // Calculate how many subtasks were added const subtasksAdded = updatedTask.subtasks ? updatedTask.subtasks.length - subtasksCountBefore : 0; // Return the result, including telemetryData log.info( `Successfully expanded task ${taskId} with ${subtasksAdded} new subtasks` ); return { success: true, data: { task: coreResult.task, subtasksAdded, hasExistingSubtasks, telemetryData: coreResult.telemetryData, tagInfo: coreResult.tagInfo } }; } catch (error) { // Make sure to restore normal logging even if there's an error if (!wasSilent && isSilentMode()) disableSilentMode(); log.error(`Error expanding task: ${error.message}`); return { success: false, error: { code: 'CORE_FUNCTION_ERROR', message: error.message || 'Failed to expand task' } }; } } catch (error) { log.error(`Error expanding task: ${error.message}`); return { success: false, error: { code: 'CORE_FUNCTION_ERROR', message: error.message || 'Failed to expand task' } }; } }
- Core implementation of task expansion: context gathering, AI-powered subtask generation using generateObjectService, complexity integration, file persistence.async function expandTask( tasksPath, taskId, numSubtasks, useResearch = false, additionalContext = '', context = {}, force = false ) { const { session, mcpLog, projectRoot: contextProjectRoot, tag, complexityReportPath } = context; const outputFormat = mcpLog ? 'json' : 'text'; // Determine projectRoot: Use from context if available, otherwise derive from tasksPath const projectRoot = contextProjectRoot || findProjectRoot(tasksPath); // Create unified logger and report function const { logger, report, isMCP } = createBridgeLogger(mcpLog, session); if (isMCP) { logger.info(`expandTask called with context: session=${!!session}`); } try { // --- BRIDGE: Try remote expansion first (API storage) --- const remoteResult = await tryExpandViaRemote({ taskId, numSubtasks, useResearch, additionalContext, force, projectRoot, tag, isMCP, outputFormat, report }); // If remote handled it, return the result if (remoteResult) { return remoteResult; } // Otherwise fall through to file-based logic below // --- End BRIDGE --- // --- Task Loading/Filtering (Unchanged) --- logger.info(`Reading tasks from ${tasksPath}`); const data = readJSON(tasksPath, projectRoot, tag); if (!data || !data.tasks) throw new Error(`Invalid tasks data in ${tasksPath}`); const taskIndex = data.tasks.findIndex( (t) => t.id === parseInt(taskId, 10) ); if (taskIndex === -1) throw new Error(`Task ${taskId} not found`); const task = data.tasks[taskIndex]; logger.info( `Expanding task ${taskId}: ${task.title}${useResearch ? ' with research' : ''}` ); // --- End Task Loading/Filtering --- // --- Handle Force Flag: Clear existing subtasks if force=true --- if (force && Array.isArray(task.subtasks) && task.subtasks.length > 0) { logger.info( `Force flag set. Clearing existing ${task.subtasks.length} subtasks for task ${taskId}.` ); task.subtasks = []; // Clear existing subtasks } // --- End Force Flag Handling --- // --- Context Gathering --- let gatheredContext = ''; try { const contextGatherer = new ContextGatherer(projectRoot, tag); const allTasksFlat = flattenTasksWithSubtasks(data.tasks); const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'expand-task'); const searchQuery = `${task.title} ${task.description}`; const searchResults = fuzzySearch.findRelevantTasks(searchQuery, { maxResults: 5, includeSelf: true }); const relevantTaskIds = fuzzySearch.getTaskIds(searchResults); const finalTaskIds = [ ...new Set([taskId.toString(), ...relevantTaskIds]) ]; if (finalTaskIds.length > 0) { const contextResult = await contextGatherer.gather({ tasks: finalTaskIds, format: 'research' }); gatheredContext = contextResult.context || ''; } } catch (contextError) { logger.warn(`Could not gather context: ${contextError.message}`); } // --- End Context Gathering --- // --- Complexity Report Integration --- let finalSubtaskCount; let complexityReasoningContext = ''; let taskAnalysis = null; logger.info( `Looking for complexity report at: ${complexityReportPath}${tag !== 'master' ? ` (tag-specific for '${tag}')` : ''}` ); try { if (fs.existsSync(complexityReportPath)) { const complexityReport = readJSON(complexityReportPath); taskAnalysis = complexityReport?.complexityAnalysis?.find( (a) => a.taskId === task.id ); if (taskAnalysis) { logger.info( `Found complexity analysis for task ${task.id}: Score ${taskAnalysis.complexityScore}` ); if (taskAnalysis.reasoning) { complexityReasoningContext = `\nComplexity Analysis Reasoning: ${taskAnalysis.reasoning}`; } } else { logger.info( `No complexity analysis found for task ${task.id} in report.` ); } } else { logger.info( `Complexity report not found at ${complexityReportPath}. Skipping complexity check.` ); } } catch (reportError) { logger.warn( `Could not read or parse complexity report: ${reportError.message}. Proceeding without it.` ); } // Determine final subtask count const explicitNumSubtasks = parseInt(numSubtasks, 10); if (!Number.isNaN(explicitNumSubtasks) && explicitNumSubtasks >= 0) { finalSubtaskCount = explicitNumSubtasks; logger.info( `Using explicitly provided subtask count: ${finalSubtaskCount}` ); } else if (taskAnalysis?.recommendedSubtasks) { finalSubtaskCount = parseInt(taskAnalysis.recommendedSubtasks, 10); logger.info( `Using subtask count from complexity report: ${finalSubtaskCount}` ); } else { finalSubtaskCount = getDefaultSubtasks(session); logger.info(`Using default number of subtasks: ${finalSubtaskCount}`); } if (Number.isNaN(finalSubtaskCount) || finalSubtaskCount < 0) { logger.warn( `Invalid subtask count determined (${finalSubtaskCount}), defaulting to 3.` ); finalSubtaskCount = 3; } // Determine prompt content AND system prompt // Calculate the next subtask ID to match current behavior: // - Start from the number of existing subtasks + 1 // - This creates sequential IDs: 1, 2, 3, 4... // - Display format shows as parentTaskId.subtaskId (e.g., "1.1", "1.2", "2.1") const nextSubtaskId = (task.subtasks?.length || 0) + 1; // Load prompts using PromptManager const promptManager = getPromptManager(); // Check if a codebase analysis provider is being used const hasCodebaseAnalysisCapability = hasCodebaseAnalysis( useResearch, projectRoot, session ); // Combine all context sources into a single additionalContext parameter let combinedAdditionalContext = ''; if (additionalContext || complexityReasoningContext) { combinedAdditionalContext = `\n\n${additionalContext}${complexityReasoningContext}`.trim(); } if (gatheredContext) { combinedAdditionalContext = `${combinedAdditionalContext}\n\n# Project Context\n\n${gatheredContext}`.trim(); } // Ensure expansionPrompt is a string (handle both string and object formats) let expansionPromptText = undefined; if (taskAnalysis?.expansionPrompt) { if (typeof taskAnalysis.expansionPrompt === 'string') { expansionPromptText = taskAnalysis.expansionPrompt; } else if ( typeof taskAnalysis.expansionPrompt === 'object' && taskAnalysis.expansionPrompt.text ) { expansionPromptText = taskAnalysis.expansionPrompt.text; } } // Ensure gatheredContext is a string (handle both string and object formats) let gatheredContextText = gatheredContext; if (typeof gatheredContext === 'object' && gatheredContext !== null) { if (gatheredContext.data) { gatheredContextText = gatheredContext.data; } else if (gatheredContext.text) { gatheredContextText = gatheredContext.text; } else { gatheredContextText = JSON.stringify(gatheredContext); } } const promptParams = { task: task, subtaskCount: finalSubtaskCount, nextSubtaskId: nextSubtaskId, additionalContext: additionalContext, complexityReasoningContext: complexityReasoningContext, gatheredContext: gatheredContextText || '', useResearch: useResearch, expansionPrompt: expansionPromptText || undefined, hasCodebaseAnalysis: hasCodebaseAnalysisCapability, projectRoot: projectRoot || '' }; let variantKey = 'default'; if (expansionPromptText) { variantKey = 'complexity-report'; logger.info( `Using expansion prompt from complexity report for task ${task.id}.` ); } else if (useResearch) { variantKey = 'research'; logger.info(`Using research variant for task ${task.id}.`); } else { logger.info(`Using standard prompt generation for task ${task.id}.`); } const { systemPrompt, userPrompt: promptContent } = promptManager.loadPrompt('expand-task', promptParams, variantKey); // Debug logging to identify the issue logger.debug(`Selected variant: ${variantKey}`); logger.debug( `Prompt params passed: ${JSON.stringify(promptParams, null, 2)}` ); logger.debug( `System prompt (first 500 chars): ${systemPrompt.substring(0, 500)}...` ); logger.debug( `User prompt (first 500 chars): ${promptContent.substring(0, 500)}...` ); // --- End Complexity Report / Prompt Logic --- // --- AI Subtask Generation using generateObjectService --- let generatedSubtasks = []; let loadingIndicator = null; if (outputFormat === 'text') { loadingIndicator = startLoadingIndicator( `Generating ${finalSubtaskCount || 'appropriate number of'} subtasks...\n` ); } let aiServiceResponse = null; try { const role = useResearch ? 'research' : 'main'; // Call generateObjectService with the determined prompts and telemetry params aiServiceResponse = await generateObjectService({ prompt: promptContent, systemPrompt: systemPrompt, role, session, projectRoot, schema: COMMAND_SCHEMAS['expand-task'], objectName: 'subtasks', commandName: 'expand-task', outputType: outputFormat }); // With generateObject, we expect structured data – verify it before use const mainResult = aiServiceResponse?.mainResult; if (!mainResult || !Array.isArray(mainResult.subtasks)) { throw new Error('AI response did not include a valid subtasks array.'); } generatedSubtasks = mainResult.subtasks; logger.info(`Received ${generatedSubtasks.length} subtasks from AI.`); } catch (error) { if (loadingIndicator) stopLoadingIndicator(loadingIndicator); logger.error( `Error during AI call or parsing for task ${taskId}: ${error.message}`, // Added task ID context 'error' ); throw error; } finally { if (loadingIndicator) stopLoadingIndicator(loadingIndicator); } // --- Task Update & File Writing --- // Ensure task.subtasks is an array before appending if (!Array.isArray(task.subtasks)) { task.subtasks = []; } // Append the newly generated and validated subtasks task.subtasks.push(...generatedSubtasks); // --- End Change: Append instead of replace --- data.tasks[taskIndex] = task; // Assign the modified task back writeJSON(tasksPath, data, projectRoot, tag); // await generateTaskFiles(tasksPath, path.dirname(tasksPath)); // Display AI Usage Summary for CLI if ( outputFormat === 'text' && aiServiceResponse && aiServiceResponse.telemetryData ) { displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli'); } // Return the updated task object AND telemetry data return { task, telemetryData: aiServiceResponse?.telemetryData, tagInfo: aiServiceResponse?.tagInfo }; } catch (error) { // Catches errors from file reading, parsing, AI call etc. logger.error(`Error expanding task ${taskId}: ${error.message}`, 'error'); if (outputFormat === 'text' && getDebugFlag(session)) { console.error(error); // Log full stack in debug CLI mode } throw error; // Re-throw for the caller } } export default expandTask;
- src/schemas/expand-task.js:4-7 (schema)Zod schema for the expand-task tool response, defining subtasks array.export const ExpandTaskResponseSchema = z.object({ subtasks: z.array(SubtaskSchema) });