fix_dependencies
Automatically resolve invalid dependencies in task files to ensure project consistency. Specify the project root and task file path to streamline task management workflows.
Instructions
Fix invalid dependencies in tasks automatically
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| file | No | Absolute path to the tasks file | |
| projectRoot | Yes | The directory of the project. Must be an absolute path. | |
| tag | No | Tag context to operate on |
Implementation Reference
- mcp-server/src/tools/fix-dependencies.js:16-83 (registration)Registration function that adds the 'fix_dependencies' tool to the MCP server, including schema (parameters) and handler (execute). This is the primary definition of the MCP tool.export function registerFixDependenciesTool(server) { server.addTool({ name: 'fix_dependencies', description: 'Fix invalid dependencies in tasks automatically', parameters: z.object({ 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: withToolContext('fix-dependencies', async (args, context) => { try { context.log.info( `Fixing dependencies 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 }, context.log ); } catch (error) { context.log.error(`Error finding tasks.json: ${error.message}`); return createErrorResponse( `Failed to find tasks.json: ${error.message}` ); } const result = await fixDependenciesDirect( { tasksJsonPath: tasksJsonPath, projectRoot: args.projectRoot, tag: resolvedTag }, context.log ); if (result.success) { context.log.info( `Successfully fixed dependencies: ${result.data.message}` ); } else { context.log.error( `Failed to fix dependencies: ${result.error.message}` ); } return handleApiResult({ result, log: context.log, errorPrefix: 'Error fixing dependencies', projectRoot: args.projectRoot }); } catch (error) { context.log.error(`Error in fixDependencies tool: ${error.message}`); return createErrorResponse(error.message); } }) }); }
- The core execution handler for the fix_dependencies tool. Handles input parameters, resolves paths and tags, locates tasks.json, calls the direct fix function, and returns structured results.execute: withToolContext('fix-dependencies', async (args, context) => { try { context.log.info( `Fixing dependencies 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 }, context.log ); } catch (error) { context.log.error(`Error finding tasks.json: ${error.message}`); return createErrorResponse( `Failed to find tasks.json: ${error.message}` ); } const result = await fixDependenciesDirect( { tasksJsonPath: tasksJsonPath, projectRoot: args.projectRoot, tag: resolvedTag }, context.log ); if (result.success) { context.log.info( `Successfully fixed dependencies: ${result.data.message}` ); } else { context.log.error( `Failed to fix dependencies: ${result.error.message}` ); } return handleApiResult({ result, log: context.log, errorPrefix: 'Error fixing dependencies', projectRoot: args.projectRoot }); } catch (error) { context.log.error(`Error in fixDependencies tool: ${error.message}`); return createErrorResponse(error.message); } })
- Direct function wrapper that calls the underlying fixDependenciesCommand from scripts/modules/dependency-manager.js, handles file existence checks, silent mode, and returns structured success/error responses.export async function fixDependenciesDirect(args, log) { // Destructure expected args const { tasksJsonPath, projectRoot, tag } = args; try { log.info(`Fixing invalid dependencies in tasks: ${tasksJsonPath}`); // Check if tasksJsonPath was provided if (!tasksJsonPath) { log.error('fixDependenciesDirect called without tasksJsonPath'); return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'tasksJsonPath is required' } }; } // Use provided path const tasksPath = tasksJsonPath; // Verify the file exists if (!fs.existsSync(tasksPath)) { return { success: false, error: { code: 'FILE_NOT_FOUND', message: `Tasks file not found at ${tasksPath}` } }; } // Enable silent mode to prevent console logs from interfering with JSON response enableSilentMode(); const options = { projectRoot, tag }; // Call the original command function using the provided path and proper context await fixDependenciesCommand(tasksPath, options); // Restore normal logging disableSilentMode(); return { success: true, data: { message: 'Dependencies fixed successfully', tasksPath, tag: tag || 'master' } }; } catch (error) { // Make sure to restore normal logging even if there's an error disableSilentMode(); log.error(`Error fixing dependencies: ${error.message}`); return { success: false, error: { code: 'FIX_DEPENDENCIES_ERROR', message: error.message } }; } }
- mcp-server/src/tools/tool-registry.js:17-87 (registration)Central tool registry that maps the 'fix_dependencies' tool name to its registration function, allowing dynamic tool registration.import { registerFixDependenciesTool } from './fix-dependencies.js'; import { registerInitializeProjectTool } from './initialize-project.js'; import { registerListTagsTool } from './list-tags.js'; import { registerModelsTool } from './models.js'; import { registerMoveTaskTool } from './move-task.js'; import { registerNextTaskTool } from './next-task.js'; import { registerParsePRDTool } from './parse-prd.js'; import { registerRemoveDependencyTool } from './remove-dependency.js'; import { registerRemoveSubtaskTool } from './remove-subtask.js'; import { registerRemoveTaskTool } from './remove-task.js'; import { registerRenameTagTool } from './rename-tag.js'; import { registerResearchTool } from './research.js'; import { registerResponseLanguageTool } from './response-language.js'; import { registerRulesTool } from './rules.js'; import { registerScopeDownTool } from './scope-down.js'; import { registerScopeUpTool } from './scope-up.js'; 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, remove_task: registerRemoveTaskTool, remove_subtask: registerRemoveSubtaskTool, clear_subtasks: registerClearSubtasksTool, move_task: registerMoveTaskTool, add_dependency: registerAddDependencyTool, remove_dependency: registerRemoveDependencyTool, validate_dependencies: registerValidateDependenciesTool, fix_dependencies: registerFixDependenciesTool,
- The core command function implementing the dependency fixing logic: removes duplicates, invalid/missing dependencies, self-dependencies, and breaks circular dependencies in tasks.json. Called by the direct function.async function fixDependenciesCommand(tasksPath, options = {}) { const { context = {} } = options; log('info', 'Checking for and fixing invalid dependencies in tasks.json...'); try { // Read tasks data const data = readJSON(tasksPath, context.projectRoot, context.tag); if (!data || !data.tasks) { log('error', 'No valid tasks found in tasks.json'); process.exit(1); } // Create a deep copy of the original data for comparison const originalData = JSON.parse(JSON.stringify(data)); // Track fixes for reporting const stats = { nonExistentDependenciesRemoved: 0, selfDependenciesRemoved: 0, duplicateDependenciesRemoved: 0, circularDependenciesFixed: 0, tasksFixed: 0, subtasksFixed: 0 }; // First phase: Remove duplicate dependencies in tasks data.tasks.forEach((task) => { if (task.dependencies && Array.isArray(task.dependencies)) { const uniqueDeps = new Set(); const originalLength = task.dependencies.length; task.dependencies = task.dependencies.filter((depId) => { const depIdStr = String(depId); if (uniqueDeps.has(depIdStr)) { log( 'info', `Removing duplicate dependency from task ${task.id}: ${depId}` ); stats.duplicateDependenciesRemoved++; return false; } uniqueDeps.add(depIdStr); return true; }); if (task.dependencies.length < originalLength) { stats.tasksFixed++; } } // Check for duplicates in subtasks if (task.subtasks && Array.isArray(task.subtasks)) { task.subtasks.forEach((subtask) => { if (subtask.dependencies && Array.isArray(subtask.dependencies)) { const uniqueDeps = new Set(); const originalLength = subtask.dependencies.length; subtask.dependencies = subtask.dependencies.filter((depId) => { let depIdStr = String(depId); if (typeof depId === 'number' && depId < 100) { depIdStr = `${task.id}.${depId}`; } if (uniqueDeps.has(depIdStr)) { log( 'info', `Removing duplicate dependency from subtask ${task.id}.${subtask.id}: ${depId}` ); stats.duplicateDependenciesRemoved++; return false; } uniqueDeps.add(depIdStr); return true; }); if (subtask.dependencies.length < originalLength) { stats.subtasksFixed++; } } }); } }); // Create validity maps for tasks and subtasks const validTaskIds = new Set(data.tasks.map((t) => t.id)); const validSubtaskIds = new Set(); data.tasks.forEach((task) => { if (task.subtasks && Array.isArray(task.subtasks)) { task.subtasks.forEach((subtask) => { validSubtaskIds.add(`${task.id}.${subtask.id}`); }); } }); // Second phase: Remove invalid task dependencies (non-existent tasks) data.tasks.forEach((task) => { if (task.dependencies && Array.isArray(task.dependencies)) { const originalLength = task.dependencies.length; task.dependencies = task.dependencies.filter((depId) => { const isSubtask = typeof depId === 'string' && depId.includes('.'); if (isSubtask) { // Check if the subtask exists if (!validSubtaskIds.has(depId)) { log( 'info', `Removing invalid subtask dependency from task ${task.id}: ${depId} (subtask does not exist)` ); stats.nonExistentDependenciesRemoved++; return false; } return true; } else { // Check if the task exists const numericId = typeof depId === 'string' ? parseInt(depId, 10) : depId; if (!validTaskIds.has(numericId)) { log( 'info', `Removing invalid task dependency from task ${task.id}: ${depId} (task does not exist)` ); stats.nonExistentDependenciesRemoved++; return false; } return true; } }); if (task.dependencies.length < originalLength) { stats.tasksFixed++; } } // Check subtask dependencies for invalid references if (task.subtasks && Array.isArray(task.subtasks)) { task.subtasks.forEach((subtask) => { if (subtask.dependencies && Array.isArray(subtask.dependencies)) { const originalLength = subtask.dependencies.length; const subtaskId = `${task.id}.${subtask.id}`; // First check for self-dependencies const hasSelfDependency = subtask.dependencies.some((depId) => { if (typeof depId === 'string' && depId.includes('.')) { return depId === subtaskId; } else if (typeof depId === 'number' && depId < 100) { return depId === subtask.id; } return false; }); if (hasSelfDependency) { subtask.dependencies = subtask.dependencies.filter((depId) => { const normalizedDepId = typeof depId === 'number' && depId < 100 ? `${task.id}.${depId}` : String(depId); if (normalizedDepId === subtaskId) { log( 'info', `Removing self-dependency from subtask ${subtaskId}` ); stats.selfDependenciesRemoved++; return false; } return true; }); } // Then check for non-existent dependencies subtask.dependencies = subtask.dependencies.filter((depId) => { if (typeof depId === 'string' && depId.includes('.')) { if (!validSubtaskIds.has(depId)) { log( 'info', `Removing invalid subtask dependency from subtask ${subtaskId}: ${depId} (subtask does not exist)` ); stats.nonExistentDependenciesRemoved++; return false; } return true; } // Handle numeric dependencies const numericId = typeof depId === 'number' ? depId : parseInt(depId, 10); // Small numbers likely refer to subtasks in the same task if (numericId < 100) { const fullSubtaskId = `${task.id}.${numericId}`; if (!validSubtaskIds.has(fullSubtaskId)) { log( 'info', `Removing invalid subtask dependency from subtask ${subtaskId}: ${numericId}` ); stats.nonExistentDependenciesRemoved++; return false; } return true; } // Otherwise it's a task reference if (!validTaskIds.has(numericId)) { log( 'info', `Removing invalid task dependency from subtask ${subtaskId}: ${numericId}` ); stats.nonExistentDependenciesRemoved++; return false; } return true; }); if (subtask.dependencies.length < originalLength) { stats.subtasksFixed++; } } }); } }); // Third phase: Check for circular dependencies log('info', 'Checking for circular dependencies...'); // Build the dependency map for subtasks const subtaskDependencyMap = new Map(); data.tasks.forEach((task) => { if (task.subtasks && Array.isArray(task.subtasks)) { task.subtasks.forEach((subtask) => { const subtaskId = `${task.id}.${subtask.id}`; if (subtask.dependencies && Array.isArray(subtask.dependencies)) { const normalizedDeps = subtask.dependencies.map((depId) => { if (typeof depId === 'string' && depId.includes('.')) { return depId; } else if (typeof depId === 'number' && depId < 100) { return `${task.id}.${depId}`; } return String(depId); }); subtaskDependencyMap.set(subtaskId, normalizedDeps); } else { subtaskDependencyMap.set(subtaskId, []); } }); } }); // Check for and fix circular dependencies for (const [subtaskId, dependencies] of subtaskDependencyMap.entries()) { const visited = new Set(); const recursionStack = new Set(); // Detect cycles const cycleEdges = findCycles( subtaskId, subtaskDependencyMap, visited, recursionStack ); if (cycleEdges.length > 0) { const [taskId, subtaskNum] = subtaskId .split('.') .map((part) => Number(part)); const task = data.tasks.find((t) => t.id === taskId); if (task && task.subtasks) { const subtask = task.subtasks.find((st) => st.id === subtaskNum); if (subtask && subtask.dependencies) { const originalLength = subtask.dependencies.length; const edgesToRemove = cycleEdges.map((edge) => { if (edge.includes('.')) { const [depTaskId, depSubtaskId] = edge .split('.') .map((part) => Number(part)); if (depTaskId === taskId) { return depSubtaskId; } return edge; } return Number(edge); }); subtask.dependencies = subtask.dependencies.filter((depId) => { const normalizedDepId = typeof depId === 'number' && depId < 100 ? `${taskId}.${depId}` : String(depId); if ( edgesToRemove.includes(depId) || edgesToRemove.includes(normalizedDepId) ) { log( 'info', `Breaking circular dependency: Removing ${normalizedDepId} from subtask ${subtaskId}` ); stats.circularDependenciesFixed++; return false; } return true; }); if (subtask.dependencies.length < originalLength) { stats.subtasksFixed++; } } } } } // Check if any changes were made by comparing with original data const dataChanged = JSON.stringify(data) !== JSON.stringify(originalData); if (dataChanged) { // Save the changes writeJSON(tasksPath, data, context.projectRoot, context.tag); log('success', 'Fixed dependency issues in tasks.json'); // Regenerate task files log('info', 'Regenerating task files to reflect dependency changes...'); // await generateTaskFiles(tasksPath, path.dirname(tasksPath)); } else { log('info', 'No changes needed to fix dependencies'); } // Show detailed statistics report const totalFixedAll = stats.nonExistentDependenciesRemoved + stats.selfDependenciesRemoved + stats.duplicateDependenciesRemoved + stats.circularDependenciesFixed; if (!isSilentMode()) { if (totalFixedAll > 0) { log('success', `Fixed ${totalFixedAll} dependency issues in total!`); console.log( boxen( chalk.green(`Dependency Fixes Summary:\n\n`) + `${chalk.cyan('Invalid dependencies removed:')} ${stats.nonExistentDependenciesRemoved}\n` + `${chalk.cyan('Self-dependencies removed:')} ${stats.selfDependenciesRemoved}\n` + `${chalk.cyan('Duplicate dependencies removed:')} ${stats.duplicateDependenciesRemoved}\n` + `${chalk.cyan('Circular dependencies fixed:')} ${stats.circularDependenciesFixed}\n\n` + `${chalk.cyan('Tasks fixed:')} ${stats.tasksFixed}\n` + `${chalk.cyan('Subtasks fixed:')} ${stats.subtasksFixed}\n`, { padding: 1, borderColor: 'green', borderStyle: 'round', margin: { top: 1, bottom: 1 } } ) ); } else { log( 'success', 'No dependency issues found - all dependencies are valid' ); console.log( boxen( chalk.green(`All Dependencies Are Valid\n\n`) + `${chalk.cyan('Tasks checked:')} ${data.tasks.length}\n` + `${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`, { padding: 1, borderColor: 'green', borderStyle: 'round', margin: { top: 1, bottom: 1 } } ) ); } } } catch (error) { log('error', 'Error in fix-dependencies command:', error); process.exit(1); }