add_task
Create and manage tasks efficiently with AI-driven task generation in the Task Master MCP server. Define task details manually or use prompts for automated creation, ensuring structured and prioritized task management.
Instructions
Add a new task using AI
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| dependencies | No | Comma-separated list of task IDs this task depends on | |
| description | No | Task description (for manual task creation) | |
| details | No | Implementation details (for manual task creation) | |
| file | No | Path to the tasks file (default: tasks/tasks.json) | |
| priority | No | Task priority (high, medium, low) | |
| projectRoot | Yes | The directory of the project. Must be an absolute path. | |
| prompt | No | Description of the task to add (required if not using manual fields) | |
| research | No | Whether to use research capabilities for task creation | |
| tag | No | Tag context to operate on | |
| testStrategy | No | Test strategy (for manual task creation) | |
| title | No | Task title (for manual task creation) |
Implementation Reference
- mcp-server/src/tools/add-task.js:20-122 (registration)Registers the MCP 'add_task' tool including input schema (parameters) and execute handler that normalizes project root, finds tasks path, and delegates to addTaskDirectexport function registerAddTaskTool(server) { server.addTool({ name: 'add_task', description: 'Add a new task using AI', parameters: z.object({ prompt: z .string() .optional() .describe( 'Description of the task to add (required if not using manual fields)' ), title: z .string() .optional() .describe('Task title (for manual task creation)'), description: z .string() .optional() .describe('Task description (for manual task creation)'), details: z .string() .optional() .describe('Implementation details (for manual task creation)'), testStrategy: z .string() .optional() .describe('Test strategy (for manual task creation)'), dependencies: z .string() .optional() .describe('Comma-separated list of task IDs this task depends on'), priority: z .string() .optional() .describe('Task priority (high, medium, low)'), file: z .string() .optional() .describe('Path to the tasks file (default: tasks/tasks.json)'), projectRoot: z .string() .describe('The directory of the project. Must be an absolute path.'), tag: z.string().optional().describe('Tag context to operate on'), research: z .boolean() .optional() .describe('Whether to use research capabilities for task creation') }), execute: withNormalizedProjectRoot(async (args, { log, session }) => { try { log.info(`Starting add-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}` ); } // Call the direct function const result = await addTaskDirect( { tasksJsonPath: tasksJsonPath, prompt: args.prompt, title: args.title, description: args.description, details: args.details, testStrategy: args.testStrategy, dependencies: args.dependencies, priority: args.priority, research: args.research, projectRoot: args.projectRoot, tag: resolvedTag }, log, { session } ); return handleApiResult({ result, log: log, errorPrefix: 'Error adding task', projectRoot: args.projectRoot }); } catch (error) { log.error(`Error in add-task tool: ${error.message}`); return createErrorResponse(error.message); } }) }); }
- Wrapper function addTaskDirect that handles MCP-specific logging, validation, manual vs AI task creation, and calls the core addTask functionexport async function addTaskDirect(args, log, context = {}) { // Destructure expected args (including research and projectRoot) const { tasksJsonPath, prompt, dependencies, priority, research, projectRoot, tag } = args; const { session } = context; // Destructure session from context // Enable silent mode to prevent console logs from interfering with JSON response enableSilentMode(); // Create logger wrapper using the utility const mcpLog = createLogWrapper(log); try { // Check if tasksJsonPath was provided if (!tasksJsonPath) { log.error('addTaskDirect called without tasksJsonPath'); disableSilentMode(); // Disable before returning return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'tasksJsonPath is required' } }; } // Use provided path const tasksPath = tasksJsonPath; // Check if this is manual task creation or AI-driven task creation const isManualCreation = args.title && args.description; // Check required parameters if (!args.prompt && !isManualCreation) { log.error( 'Missing required parameters: either prompt or title+description must be provided' ); disableSilentMode(); return { success: false, error: { code: 'MISSING_PARAMETER', message: 'Either the prompt parameter or both title and description parameters are required for adding a task' } }; } // Extract and prepare parameters const taskDependencies = Array.isArray(dependencies) ? dependencies // Already an array if passed directly : dependencies // Check if dependencies exist and are a string ? String(dependencies) .split(',') .map((id) => parseInt(id.trim(), 10)) // Split, trim, and parse : []; // Default to empty array if null/undefined const taskPriority = priority || 'medium'; // Default priority let manualTaskData = null; let newTaskId; let telemetryData; let tagInfo; if (isManualCreation) { // Create manual task data object manualTaskData = { title: args.title, description: args.description, details: args.details || '', testStrategy: args.testStrategy || '' }; log.info( `Adding new task manually with title: "${args.title}", dependencies: [${taskDependencies.join(', ')}], priority: ${priority}` ); // Call the addTask function with manual task data const result = await addTask( tasksPath, null, // prompt is null for manual creation taskDependencies, taskPriority, { session, mcpLog, projectRoot, commandName: 'add-task', outputType: 'mcp', tag }, 'json', // outputFormat manualTaskData, // Pass the manual task data false // research flag is false for manual creation ); newTaskId = result.newTaskId; telemetryData = result.telemetryData; tagInfo = result.tagInfo; } else { // AI-driven task creation log.info( `Adding new task with prompt: "${prompt}", dependencies: [${taskDependencies.join(', ')}], priority: ${taskPriority}, research: ${research}` ); // Call the addTask function, passing the research flag const result = await addTask( tasksPath, prompt, // Use the prompt for AI creation taskDependencies, taskPriority, { session, mcpLog, projectRoot, commandName: 'add-task', outputType: 'mcp', tag }, 'json', // outputFormat null, // manualTaskData is null for AI creation research // Pass the research flag ); newTaskId = result.newTaskId; telemetryData = result.telemetryData; tagInfo = result.tagInfo; } // Restore normal logging disableSilentMode(); return { success: true, data: { taskId: newTaskId, message: `Successfully added new task #${newTaskId}`, telemetryData: telemetryData, tagInfo: tagInfo } }; } catch (error) { // Make sure to restore normal logging even if there's an error disableSilentMode(); log.error(`Error in addTaskDirect: ${error.message}`); // Add specific error code checks if needed return { success: false, error: { code: error.code || 'ADD_TASK_ERROR', // Use error code if available message: error.message } }; } }
- Core addTask implementation handling both manual and AI-driven task creation, including context gathering, prompt loading, AI service call with schema validation, dependency graph building, task persistence, and telemetryasync function addTask( tasksPath, prompt, dependencies = [], priority = null, context = {}, outputFormat = 'text', // Default to text for CLI manualTaskData = null, useResearch = false ) { const { session, mcpLog, projectRoot, commandName, outputType, tag } = context; const isMCP = !!mcpLog; // Create a consistent logFn object regardless of context const logFn = isMCP ? mcpLog // Use MCP logger if provided : { // Create a wrapper around consoleLog for CLI info: (...args) => consoleLog('info', ...args), warn: (...args) => consoleLog('warn', ...args), error: (...args) => consoleLog('error', ...args), debug: (...args) => consoleLog('debug', ...args), success: (...args) => consoleLog('success', ...args) }; // Validate priority - only accept high, medium, or low let effectivePriority = priority || getDefaultPriority(projectRoot) || DEFAULT_TASK_PRIORITY; // If priority is provided, validate and normalize it if (priority) { const normalizedPriority = normalizeTaskPriority(priority); if (normalizedPriority) { effectivePriority = normalizedPriority; } else { if (outputFormat === 'text') { consoleLog( 'warn', `Invalid priority "${priority}". Using default priority "${DEFAULT_TASK_PRIORITY}".` ); } effectivePriority = DEFAULT_TASK_PRIORITY; } } logFn.info( `Adding new task with prompt: "${prompt}", Priority: ${effectivePriority}, Dependencies: ${dependencies.join(', ') || 'None'}, Research: ${useResearch}, ProjectRoot: ${projectRoot}` ); if (tag) { logFn.info(`Using tag context: ${tag}`); } let loadingIndicator = null; let aiServiceResponse = null; // To store the full response from AI service // Create custom reporter that checks for MCP log const report = (message, level = 'info') => { if (mcpLog) { mcpLog[level](message); } else if (outputFormat === 'text') { consoleLog(level, message); } }; /** * Recursively builds a dependency graph for a given task * @param {Array} tasks - All tasks from tasks.json * @param {number} taskId - ID of the task to analyze * @param {Set} visited - Set of already visited task IDs * @param {Map} depthMap - Map of task ID to its depth in the graph * @param {number} depth - Current depth in the recursion * @return {Object} Dependency graph data */ function buildDependencyGraph( tasks, taskId, visited = new Set(), depthMap = new Map(), depth = 0 ) { // Skip if we've already visited this task or it doesn't exist if (visited.has(taskId)) { return null; } // Find the task const task = tasks.find((t) => t.id === taskId); if (!task) { return null; } // Mark as visited visited.add(taskId); // Update depth if this is a deeper path to this task if (!depthMap.has(taskId) || depth < depthMap.get(taskId)) { depthMap.set(taskId, depth); } // Process dependencies const dependencyData = []; if (task.dependencies && task.dependencies.length > 0) { for (const depId of task.dependencies) { const depData = buildDependencyGraph( tasks, depId, visited, depthMap, depth + 1 ); if (depData) { dependencyData.push(depData); } } } return { id: task.id, title: task.title, description: task.description, status: task.status, dependencies: dependencyData }; } try { // Read the existing tasks - IMPORTANT: Read the raw data without tag resolution 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; } // If file doesn't exist or is invalid, create a new structure in memory if (!rawData) { report( 'tasks.json not found or invalid. Initializing new structure.', 'info' ); rawData = { master: { tasks: [], metadata: { created: new Date().toISOString(), description: 'Default tasks context' } } }; // Do not write the file here; it will be written later with the new task. } // Handle legacy format migration using utilities if (rawData && Array.isArray(rawData.tasks) && !rawData._rawTaggedData) { report('Legacy format detected. Migrating to tagged format...', 'info'); // This is legacy format - migrate it to tagged format rawData = { master: { tasks: rawData.tasks, metadata: rawData.metadata || { created: new Date().toISOString(), updated: new Date().toISOString(), description: 'Tasks for master context' } } }; // Ensure proper metadata using utility ensureTagMetadata(rawData.master, { description: 'Tasks for master context' }); // Do not write the file here; it will be written later with the new task. // Perform complete migration (config.json, state.json) performCompleteTagMigration(tasksPath); markMigrationForNotice(tasksPath); report('Successfully migrated to tagged format.', 'success'); } // Use the provided tag, or the current active tag, or default to 'master' const targetTag = tag; // Ensure the target tag exists if (!rawData[targetTag]) { report( `Tag "${targetTag}" does not exist. Please create it first using the 'add-tag' command.`, 'error' ); throw new Error(`Tag "${targetTag}" not found.`); } // Ensure the target tag has a tasks array and metadata object if (!rawData[targetTag].tasks) { rawData[targetTag].tasks = []; } if (!rawData[targetTag].metadata) { rawData[targetTag].metadata = { created: new Date().toISOString(), updated: new Date().toISOString(), description: `` }; } // Get a flat list of ALL tasks across ALL tags to validate dependencies const allTasks = getAllTasks(rawData); // Find the highest task ID *within the target tag* to determine the next ID const tasksInTargetTag = rawData[targetTag].tasks; const highestId = tasksInTargetTag.length > 0 ? Math.max(...tasksInTargetTag.map((t) => t.id)) : 0; const newTaskId = highestId + 1; // Only show UI box for CLI mode if (outputFormat === 'text') { console.log( boxen(chalk.white.bold(`Creating New Task #${newTaskId}`), { padding: 1, borderColor: 'blue', borderStyle: 'round', margin: { top: 1, bottom: 1 } }) ); } // Validate dependencies before proceeding const invalidDeps = dependencies.filter((depId) => { // Ensure depId is parsed as a number for comparison const numDepId = parseInt(depId, 10); return Number.isNaN(numDepId) || !allTasks.some((t) => t.id === numDepId); }); if (invalidDeps.length > 0) { report( `The following dependencies do not exist or are invalid: ${invalidDeps.join(', ')}`, 'warn' ); report('Removing invalid dependencies...', 'info'); dependencies = dependencies.filter( (depId) => !invalidDeps.includes(depId) ); } // Ensure dependencies are numbers const numericDependencies = dependencies.map((dep) => parseInt(dep, 10)); // Build dependency graphs for explicitly specified dependencies const dependencyGraphs = []; const allRelatedTaskIds = new Set(); const depthMap = new Map(); // First pass: build a complete dependency graph for each specified dependency for (const depId of numericDependencies) { const graph = buildDependencyGraph(allTasks, depId, new Set(), depthMap); if (graph) { dependencyGraphs.push(graph); } } // Second pass: build a set of all related task IDs for flat analysis for (const [taskId, depth] of depthMap.entries()) { allRelatedTaskIds.add(taskId); } let taskData; // Check if manual task data is provided if (manualTaskData) { report('Using manually provided task data', 'info'); taskData = manualTaskData; report('DEBUG: Taking MANUAL task data path.', 'debug'); // Basic validation for manual data if ( !taskData.title || typeof taskData.title !== 'string' || !taskData.description || typeof taskData.description !== 'string' ) { throw new Error( 'Manual task data must include at least a title and description.' ); } } else { report('DEBUG: Taking AI task generation path.', 'debug'); // --- Refactored AI Interaction --- report(`Generating task data with AI with prompt:\n${prompt}`, 'info'); // --- Use the new ContextGatherer --- const contextGatherer = new ContextGatherer(projectRoot, tag); const gatherResult = await contextGatherer.gather({ semanticQuery: prompt, dependencyTasks: numericDependencies, format: 'research' }); const gatheredContext = gatherResult.context; const analysisData = gatherResult.analysisData; // Display context analysis if not in silent mode if (outputFormat === 'text' && analysisData) { displayContextAnalysis(analysisData, prompt, gatheredContext.length); } // Add any manually provided details to the prompt for context let contextFromArgs = ''; if (manualTaskData?.title) contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`; if (manualTaskData?.description) contextFromArgs += `\n- Suggested Description: "${manualTaskData.description}"`; if (manualTaskData?.details) contextFromArgs += `\n- Additional Details Context: "${manualTaskData.details}"`; if (manualTaskData?.testStrategy) contextFromArgs += `\n- Additional Test Strategy Context: "${manualTaskData.testStrategy}"`; // Load prompts using PromptManager const promptManager = getPromptManager(); const { systemPrompt, userPrompt } = await promptManager.loadPrompt( 'add-task', { prompt, newTaskId, existingTasks: allTasks, gatheredContext, contextFromArgs, useResearch, priority: effectivePriority, dependencies: numericDependencies, hasCodebaseAnalysis: hasCodebaseAnalysis( useResearch, projectRoot, session ), projectRoot: projectRoot } ); // Start the loading indicator - only for text mode if (outputFormat === 'text') { loadingIndicator = startLoadingIndicator( `Generating new task with ${useResearch ? 'Research' : 'Main'} AI... \n` ); } try { const serviceRole = useResearch ? 'research' : 'main'; report('DEBUG: Calling generateObjectService...', 'debug'); aiServiceResponse = await generateObjectService({ // Capture the full response role: serviceRole, session: session, projectRoot: projectRoot, schema: COMMAND_SCHEMAS['add-task'], objectName: 'newTaskData', systemPrompt: systemPrompt, prompt: userPrompt, commandName: commandName || 'add-task', // Use passed commandName or default outputType: outputType || (isMCP ? 'mcp' : 'cli') // Use passed outputType or derive }); report('DEBUG: generateObjectService returned successfully.', 'debug'); if (!aiServiceResponse || !aiServiceResponse.mainResult) { throw new Error( 'AI service did not return the expected object structure.' ); } // Prefer mainResult if it looks like a valid task object, otherwise try mainResult.object if ( aiServiceResponse.mainResult.title && aiServiceResponse.mainResult.description ) { taskData = aiServiceResponse.mainResult; } else if ( aiServiceResponse.mainResult.object && aiServiceResponse.mainResult.object.title && aiServiceResponse.mainResult.object.description ) { taskData = aiServiceResponse.mainResult.object; } else { throw new Error('AI service did not return a valid task object.'); } report('Successfully generated task data from AI.', 'success'); // Success! Show checkmark if (loadingIndicator) { succeedLoadingIndicator( loadingIndicator, 'Task generated successfully' ); loadingIndicator = null; // Clear it } } catch (error) { // Failure! Show X if (loadingIndicator) { failLoadingIndicator(loadingIndicator, 'AI generation failed'); loadingIndicator = null; } report( `DEBUG: generateObjectService caught error: ${error.message}`, 'debug' ); report(`Error generating task with AI: ${error.message}`, 'error'); throw error; // Re-throw error after logging } finally { report('DEBUG: generateObjectService finally block reached.', 'debug'); // Clean up if somehow still running if (loadingIndicator) { stopLoadingIndicator(loadingIndicator); } } // --- End Refactored AI Interaction --- } // Create the new task object const newTask = { id: newTaskId, title: taskData.title, description: taskData.description, details: taskData.details || '', testStrategy: taskData.testStrategy || '', status: 'pending', dependencies: taskData.dependencies?.length ? taskData.dependencies : numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified priority: effectivePriority, subtasks: [] // Initialize with empty subtasks array }; // Additional check: validate all dependencies in the AI response if (taskData.dependencies?.length) { const allValidDeps = taskData.dependencies.every((depId) => { const numDepId = parseInt(depId, 10); return ( !Number.isNaN(numDepId) && allTasks.some((t) => t.id === numDepId) ); }); if (!allValidDeps) { report( 'AI suggested invalid dependencies. Filtering them out...', 'warn' ); newTask.dependencies = taskData.dependencies.filter((depId) => { const numDepId = parseInt(depId, 10); return ( !Number.isNaN(numDepId) && allTasks.some((t) => t.id === numDepId) ); }); } } // Add the task to the tasks array OF THE CORRECT TAG rawData[targetTag].tasks.push(newTask); // Update the tag's metadata ensureTagMetadata(rawData[targetTag], { description: `Tasks for ${targetTag} context` }); report('DEBUG: Writing tasks.json...', 'debug'); // Write the updated raw data back to the file // The writeJSON function will automatically filter out _rawTaggedData writeJSON(tasksPath, rawData, projectRoot, targetTag); report('DEBUG: tasks.json written.', 'debug'); // Show success message - only for text output (CLI) if (outputFormat === 'text') { const table = new Table({ head: [ chalk.cyan.bold('ID'), chalk.cyan.bold('Title'), chalk.cyan.bold('Description') ], colWidths: [5, 30, 50] // Adjust widths as needed }); table.push([ newTask.id, truncate(newTask.title, 27), truncate(newTask.description, 47) ]); console.log(chalk.green('✓ New task created successfully:')); console.log(table.toString()); // Helper to get priority color const getPriorityColor = (p) => { switch (p?.toLowerCase()) { case 'high': return 'red'; case 'low': return 'gray'; default: return 'yellow'; } }; // Check if AI added new dependencies that weren't explicitly provided const aiAddedDeps = newTask.dependencies.filter( (dep) => !numericDependencies.includes(dep) ); // Check if AI removed any dependencies that were explicitly provided const aiRemovedDeps = numericDependencies.filter( (dep) => !newTask.dependencies.includes(dep) ); // Get task titles for dependencies to display const depTitles = {}; newTask.dependencies.forEach((dep) => { const depTask = allTasks.find((t) => t.id === dep); if (depTask) { depTitles[dep] = truncate(depTask.title, 30); } }); // Prepare dependency display string let dependencyDisplay = ''; if (newTask.dependencies.length > 0) { dependencyDisplay = chalk.white('Dependencies:') + '\n'; newTask.dependencies.forEach((dep) => { const isAiAdded = aiAddedDeps.includes(dep); const depType = isAiAdded ? chalk.yellow(' (AI suggested)') : ''; dependencyDisplay += chalk.white( ` - ${dep}: ${depTitles[dep] || 'Unknown task'}${depType}` ) + '\n'; }); } else { dependencyDisplay = chalk.white('Dependencies: None') + '\n'; } // Add info about removed dependencies if any if (aiRemovedDeps.length > 0) { dependencyDisplay += chalk.gray('\nUser-specified dependencies that were not used:') + '\n'; aiRemovedDeps.forEach((dep) => { const depTask = allTasks.find((t) => t.id === dep); const title = depTask ? truncate(depTask.title, 30) : 'Unknown task'; dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + '\n'; }); } // Add dependency analysis summary let dependencyAnalysis = ''; if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) { dependencyAnalysis = '\n' + chalk.white.bold('Dependency Analysis:') + '\n'; if (aiAddedDeps.length > 0) { dependencyAnalysis += chalk.green( `AI identified ${aiAddedDeps.length} additional dependencies` ) + '\n'; } if (aiRemovedDeps.length > 0) { dependencyAnalysis += chalk.yellow( `AI excluded ${aiRemovedDeps.length} user-provided dependencies` ) + '\n'; } } // Show success message box console.log( boxen( chalk.white.bold(`Task ${newTaskId} Created Successfully`) + '\n\n' + chalk.white(`Title: ${newTask.title}`) + '\n' + chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) + '\n' + chalk.white( `Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}` ) + '\n\n' + dependencyDisplay + dependencyAnalysis + '\n' + chalk.white.bold('Next Steps:') + '\n' + chalk.cyan( `1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details` ) + '\n' + chalk.cyan( `2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it` ) + '\n' + chalk.cyan( `3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks` ), { padding: 1, borderColor: 'green', borderStyle: 'round' } ) ); // Display AI Usage Summary if telemetryData is available if ( aiServiceResponse && aiServiceResponse.telemetryData && (outputType === 'cli' || outputType === 'text') ) { displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli'); } } report( `DEBUG: Returning new task ID: ${newTaskId} and telemetry.`, 'debug' ); return { newTaskId: newTaskId, telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null, tagInfo: aiServiceResponse ? aiServiceResponse.tagInfo : null }; } catch (error) { // Stop any loading indicator on error if (loadingIndicator) { stopLoadingIndicator(loadingIndicator); } report(`Error adding task: ${error.message}`, 'error'); if (outputFormat === 'text') { console.error(chalk.red(`Error: ${error.message}`)); } // In MCP mode, we let the direct function handler catch and format throw error; } } export default addTask;
- src/schemas/add-task.js:4-21 (schema)Zod schema defining the structure of task data generated by AI for the add-task tool, used in generateObjectServiceexport const AddTaskResponseSchema = z.object({ title: z.string().describe('Clear, concise title for the task'), description: z .string() .describe('A one or two sentence description of the task'), details: z .string() .describe('In-depth implementation details, considerations, and guidance'), testStrategy: z .string() .describe('Detailed approach for verifying task completion'), dependencies: z .array(z.number()) .nullable() .describe( 'Array of task IDs that this task depends on (must be completed before this task can start)' ) });
- Input parameter schema for the 'add_task' MCP tool, defining all accepted arguments with Zod validationparameters: z.object({ prompt: z .string() .optional() .describe( 'Description of the task to add (required if not using manual fields)' ), title: z .string() .optional() .describe('Task title (for manual task creation)'), description: z .string() .optional() .describe('Task description (for manual task creation)'), details: z .string() .optional() .describe('Implementation details (for manual task creation)'), testStrategy: z .string() .optional() .describe('Test strategy (for manual task creation)'), dependencies: z .string() .optional() .describe('Comma-separated list of task IDs this task depends on'), priority: z .string() .optional() .describe('Task priority (high, medium, low)'), file: z .string() .optional() .describe('Path to the tasks file (default: tasks/tasks.json)'), projectRoot: z .string() .describe('The directory of the project. Must be an absolute path.'), tag: z.string().optional().describe('Tag context to operate on'), research: z .boolean() .optional() .describe('Whether to use research capabilities for task creation') }),