task-master-neo-sdlc
Version:
Enhanced task management system with Neo SDLC agents and MCP tools for comprehensive, AI-driven software development lifecycle management.
1,040 lines (902 loc) • 33.8 kB
JavaScript
import { readJSON, writeJSON } from '../../utils/file-utils.js';
import { log } from '../../utils/logging.js';
import { createQAAgent } from './agents/qa-agent.js';
import { createTDDFramework } from './tdd-framework.js';
// Environment variables with defaults
const TASKMASTER_MCP_URL = process.env.TASKMASTER_MCP_URL || 'http://localhost:3001/mcp/execute';
const TASKMASTER_PROJECT_ROOT = process.env.TASKMASTER_PROJECT_ROOT || process.cwd();
export class AgentWorkflowSystem {
constructor(knowledgeGraph) {
this.knowledgeGraph = knowledgeGraph;
this.agents = new Map();
this.workflows = new Map();
this.coordinationPatterns = {
sequential: this.validateSequentialTransition.bind(this),
parallel: this.validateParallelTransition.bind(this)
};
// TODO: Determine the definitive source for projectRoot within the Neo context
// Using TASKMASTER_PROJECT_ROOT env var or fallback to cwd for now
this.projectRoot = process.env.TASKMASTER_PROJECT_ROOT || process.cwd();
// Initialize TDD framework
this.tddFramework = createTDDFramework(this);
log.info(`AgentWorkflowSystem initialized. Project Root: ${this.projectRoot}, MCP URL: ${TASKMASTER_MCP_URL}`);
}
async registerAgent(agentData) {
const { id, capabilities, knowledgeDomains } = agentData;
if (!id || !capabilities) {
throw new Error('Agent must have id and capabilities');
}
const agent = {
...agentData,
status: 'available',
metrics: {
tasksCompleted: 0,
successRate: 1.0,
avgCompletionTime: 0,
qualityScore: 1.0
},
currentTask: null,
history: []
};
await this.knowledgeGraph.addNode({
id,
type: 'agent',
data: {
capabilities,
knowledgeDomains: knowledgeDomains || [],
status: agent.status,
metrics: agent.metrics
}
});
this.agents.set(id, agent);
return agent;
}
async createWorkflow(workflowData) {
const { id, steps, coordinationPattern = 'sequential', priority = 'medium' } = workflowData;
if (!id || !steps || !Array.isArray(steps)) {
throw new Error('Workflow must have id and steps array');
}
const workflow = {
...workflowData,
status: 'pending',
currentStep: 0,
assignedAgents: new Map(),
metrics: {
startTime: null,
completionTime: null,
totalTime: 0,
qualityScore: 0
},
history: []
};
await this.knowledgeGraph.addNode({
id,
type: 'workflow',
data: {
steps: steps.map(s => ({ ...s, status: 'pending' })),
status: workflow.status,
assignedAgents: [],
coordinationPattern,
priority
}
});
this.workflows.set(id, workflow);
return workflow;
}
async assignStep(workflowId, stepIndex, agentId) {
const workflow = this.workflows.get(workflowId);
const agent = this.agents.get(agentId);
if (!workflow || !agent) {
throw new Error('Invalid workflow or agent ID');
}
const step = workflow.steps[stepIndex];
if (!step) {
throw new Error('Invalid step index');
}
// Validate transition based on coordination pattern
const validator = this.coordinationPatterns[workflow.coordinationPattern];
if (!validator || !validator(workflow, stepIndex)) {
throw new Error('Invalid step transition for coordination pattern');
}
// Check agent capabilities
if (!this.validateAgentCapabilities(agent, step)) {
throw new Error('Agent lacks required capabilities for step');
}
// Update workflow
workflow.assignedAgents.set(stepIndex, agentId);
step.status = 'assigned';
step.assignedTo = agentId;
step.assignedAt = Date.now();
// Update agent
agent.status = 'busy';
agent.currentTask = { workflowId, stepIndex };
// Update knowledge graph
await this.knowledgeGraph.addEdge(workflowId, agentId, 'assigned_to');
await this.updateWorkflowStatus(workflowId);
return { workflow, agent };
}
validateSequentialTransition(workflow, stepIndex) {
if (stepIndex === 0) return true;
const prevStep = workflow.steps[stepIndex - 1];
return prevStep && prevStep.status === 'completed';
}
validateParallelTransition(workflow, stepIndex) {
return workflow.steps[stepIndex].status === 'pending';
}
validateAgentCapabilities(agent, step) {
return step.requiredCapabilities.every(cap =>
agent.capabilities.includes(cap)
);
}
async completeStep(workflowId, stepIndex, result) {
const workflow = this.workflows.get(workflowId);
if (!workflow) {
throw new Error('Invalid workflow ID');
}
const step = workflow.steps[stepIndex];
if (!step) {
throw new Error('Invalid step index');
}
const agentId = workflow.assignedAgents.get(stepIndex);
const agent = this.agents.get(agentId);
if (!agent) {
throw new Error('Agent not found');
}
// Validate result
const quality = await this.validateStepResult(step, result);
// Update step
step.status = 'completed';
step.completedAt = Date.now();
step.result = result;
step.quality = quality;
// Update agent metrics
agent.status = 'available';
agent.currentTask = null;
agent.metrics.tasksCompleted++;
agent.metrics.avgCompletionTime = (
(agent.metrics.avgCompletionTime * (agent.metrics.tasksCompleted - 1) +
(step.completedAt - step.assignedAt)) / agent.metrics.tasksCompleted
);
agent.metrics.qualityScore = (
(agent.metrics.qualityScore * (agent.metrics.tasksCompleted - 1) + quality) /
agent.metrics.tasksCompleted
);
// Add to history
agent.history.push({
workflowId,
stepIndex,
completedAt: step.completedAt,
quality
});
// Update workflow status
await this.updateWorkflowStatus(workflowId);
// Update knowledge graph
await this.knowledgeGraph.updateContext({
id: agentId,
type: 'agent',
metrics: agent.metrics,
status: agent.status
});
return { workflow, agent };
}
async handleStepFailure(workflowId, stepIndex, error) {
const workflow = this.workflows.get(workflowId);
if (!workflow) {
throw new Error('Invalid workflow ID');
}
const step = workflow.steps[stepIndex];
if (!step) {
throw new Error('Invalid step index');
}
const agentId = workflow.assignedAgents.get(stepIndex);
const agent = this.agents.get(agentId);
if (!agent) {
throw new Error('Agent not found');
}
// Update step
step.status = 'failed';
step.error = error;
step.failedAt = Date.now();
// Update agent metrics
agent.status = 'available';
agent.currentTask = null;
agent.metrics.successRate = (
(agent.metrics.tasksCompleted * agent.metrics.successRate) /
(agent.metrics.tasksCompleted + 1)
);
// Add to history
agent.history.push({
workflowId,
stepIndex,
failedAt: step.failedAt,
error
});
// Update workflow status
await this.updateWorkflowStatus(workflowId);
// Update knowledge graph
await this.knowledgeGraph.updateContext({
id: agentId,
type: 'agent',
metrics: agent.metrics,
status: agent.status
});
return { workflow, agent };
}
async validateStepResult(step, result) {
// Basic validation - can be extended based on step type
if (!result || typeof result !== 'object') {
return 0.5; // Mediocre quality for invalid format
}
// Check required fields
const hasRequired = step.requiredResults.every(field =>
result.hasOwnProperty(field)
);
if (!hasRequired) {
return 0.6; // Below average for missing fields
}
// Check quality metrics if defined
if (step.qualityMetrics) {
const scores = await Promise.all(
step.qualityMetrics.map(metric => this.evaluateQualityMetric(result, metric))
);
return scores.reduce((a, b) => a + b, 0) / scores.length;
}
return 0.8; // Good default quality
}
async evaluateQualityMetric(result, metric) {
// Implement specific quality metric evaluations
switch (metric.type) {
case 'completeness':
return Object.keys(result).length / metric.expectedFields;
case 'accuracy':
return metric.validateFn(result);
default:
return 0.7; // Default quality score
}
}
async updateWorkflowStatus(workflowId) {
const workflow = this.workflows.get(workflowId);
if (!workflow) {
throw new Error('Invalid workflow ID');
}
const allCompleted = workflow.steps.every(s => s.status === 'completed');
const anyFailed = workflow.steps.some(s => s.status === 'failed');
const anyBlocked = workflow.steps.some(s => s.status === 'blocked');
if (allCompleted) {
workflow.status = 'completed';
workflow.metrics.completionTime = Date.now();
workflow.metrics.totalTime = workflow.metrics.completionTime - workflow.metrics.startTime;
workflow.metrics.qualityScore = workflow.steps.reduce(
(sum, step) => sum + (step.quality || 0), 0
) / workflow.steps.length;
} else if (anyFailed) {
workflow.status = 'failed';
} else if (anyBlocked) {
workflow.status = 'blocked';
} else {
workflow.status = 'in_progress';
}
// Update knowledge graph
await this.knowledgeGraph.updateContext({
id: workflowId,
type: 'workflow',
status: workflow.status,
metrics: workflow.metrics
});
return workflow;
}
async getWorkflowStatus(workflowId) {
const workflow = this.workflows.get(workflowId);
if (!workflow) {
throw new Error('Invalid workflow ID');
}
const context = await this.knowledgeGraph.getContext(workflowId);
const analysis = await this.knowledgeGraph.analyzeContext(workflowId);
return {
...workflow,
context,
analysis
};
}
async getAgentStatus(agentId) {
const agent = this.agents.get(agentId);
if (!agent) {
throw new Error('Invalid agent ID');
}
const context = await this.knowledgeGraph.getContext(agentId);
const analysis = await this.knowledgeGraph.analyzeContext(agentId);
return {
...agent,
context,
analysis,
recentHistory: agent.history.slice(-5)
};
}
/**
* Create a workflow from Task Master tasks
* @param {Object} options - Options for creating the workflow
* @param {string} options.id - Workflow ID
* @param {Array<string>} options.taskMasterIds - Array of Task Master task IDs to include
* @param {string} options.coordinationPattern - Coordination pattern (sequential or parallel)
* @param {string} options.priority - Workflow priority
* @returns {Promise<Object>} Created workflow
*/
async createWorkflowFromTaskMaster(options) {
const { id, taskMasterIds, coordinationPattern = 'sequential', priority = 'medium' } = options;
if (!id || !taskMasterIds || !Array.isArray(taskMasterIds) || taskMasterIds.length === 0) {
throw new Error('Workflow must have id and at least one Task Master task ID');
}
// Fetch tasks from Task Master
const allTasks = await this.fetchTaskMasterTasks(null, true); // Get all tasks with subtasks
// Create workflow steps from Task Master tasks
const steps = [];
for (const taskMasterId of taskMasterIds) {
// Handle both main tasks and subtasks
if (taskMasterId.includes('.')) {
// This is a subtask ID (e.g., "5.2")
const [parentId, subtaskId] = taskMasterId.split('.');
const parentTask = allTasks.find(t => t.id === parseInt(parentId));
if (parentTask && parentTask.subtasks) {
const subtask = parentTask.subtasks.find(s => s.id === parseInt(subtaskId));
if (subtask) {
// Create step from subtask
steps.push(this.createStepFromTaskMasterSubtask(subtask, parentTask, taskMasterId));
}
}
} else {
// This is a main task ID
const task = allTasks.find(t => t.id === parseInt(taskMasterId));
if (task) {
if (task.subtasks && task.subtasks.length > 0) {
// Create steps from all subtasks
for (const subtask of task.subtasks) {
steps.push(this.createStepFromTaskMasterSubtask(subtask, task, `${task.id}.${subtask.id}`));
}
} else {
// Create step from main task
steps.push(this.createStepFromTaskMasterTask(task, taskMasterId));
}
}
}
}
// Create the workflow
return this.createWorkflow({
id,
steps,
coordinationPattern,
priority
});
}
/**
* Create a workflow step from a Task Master subtask
* @param {Object} subtask - Task Master subtask
* @param {Object} parentTask - Parent task
* @param {string} taskMasterId - Task Master ID (e.g., "5.2")
* @returns {Object} Workflow step
*/
createStepFromTaskMasterSubtask(subtask, parentTask, taskMasterId) {
// Determine required capabilities based on subtask action
const requiredCapabilities = this.getCapabilitiesForAction(subtask.action);
// Create the step
return {
id: `step-${taskMasterId.replace('.', '-')}`,
title: subtask.title,
description: subtask.description,
details: subtask.details,
taskMasterId,
requiredCapabilities,
action: subtask.action,
requiredResults: this.getRequiredResultsForAction(subtask.action)
};
}
/**
* Create a workflow step from a Task Master main task
* @param {Object} task - Task Master task
* @param {string} taskMasterId - Task Master ID
* @returns {Object} Workflow step
*/
createStepFromTaskMasterTask(task, taskMasterId) {
// For main tasks, use a generic implementation action
const action = {
type: 'implementBusinessLogic',
parameters: {
taskTitle: task.title,
taskDescription: task.description
}
};
// Create the step
return {
id: `step-${taskMasterId}`,
title: task.title,
description: task.description,
details: task.details,
taskMasterId,
requiredCapabilities: ['coding', 'problem-solving'],
action,
requiredResults: ['implementationPath', 'implementationCode']
};
}
/**
* Get required capabilities for an action
* @param {Object} action - Action object
* @returns {Array<string>} Required capabilities
*/
getCapabilitiesForAction(action) {
if (!action || !action.type) {
return ['coding', 'problem-solving'];
}
// Map action types to required capabilities
const capabilityMap = {
'createComponent': ['react', 'component-design', 'frontend'],
'implementApiLogic': ['api-design', 'backend', 'data-handling'],
'createTest': ['testing', 'quality-assurance'],
'setupDatabase': ['database', 'data-modeling'],
'configureAuthentication': ['security', 'authentication'],
'implementBusinessLogic': ['coding', 'problem-solving', 'algorithm-design'],
'createUtility': ['coding', 'software-design']
};
return capabilityMap[action.type] || ['coding', 'problem-solving'];
}
/**
* Get required results for an action
* @param {Object} action - Action object
* @returns {Array<string>} Required results
*/
getRequiredResultsForAction(action) {
if (!action || !action.type) {
return ['implementationPath', 'implementationCode'];
}
// Map action types to required results
const resultsMap = {
'createComponent': ['componentPath', 'componentCode'],
'implementApiLogic': ['apiPath', 'apiCode', 'apiDocumentation'],
'createTest': ['testPath', 'testCode', 'testResults'],
'setupDatabase': ['schemaPath', 'schemaCode', 'migrationPath'],
'configureAuthentication': ['authConfigPath', 'authImplementation'],
'implementBusinessLogic': ['implementationPath', 'implementationCode'],
'createUtility': ['utilityPath', 'utilityCode', 'utilityDocumentation']
};
return resultsMap[action.type] || ['implementationPath', 'implementationCode'];
}
/**
* Check if a task needs expansion
* @param {string} taskMasterId - Task Master ID
* @returns {Promise<Object>} Result indicating if expansion is needed
*/
async checkTaskNeedsExpansion(taskMasterId) {
// If this is a subtask ID, no expansion needed
if (taskMasterId.includes('.')) {
return { needsExpansion: false };
}
// Fetch the task
const task = await this.fetchTaskMasterTaskById(taskMasterId);
if (!task) {
throw new Error(`Task with ID ${taskMasterId} not found`);
}
// Check if task already has subtasks
if (task.subtasks && task.subtasks.length > 0) {
return { needsExpansion: false, hasSubtasks: true, subtaskCount: task.subtasks.length };
}
// Check task complexity
const complexityReport = await this.analyzeTaskComplexity([task]);
const taskComplexity = complexityReport.find(t => t.taskId === parseInt(taskMasterId));
if (taskComplexity && taskComplexity.complexityScore >= 7) {
return {
needsExpansion: true,
complexityScore: taskComplexity.complexityScore,
recommendedSubtasks: taskComplexity.recommendedSubtasks,
expansionPrompt: taskComplexity.expansionPrompt
};
}
return { needsExpansion: false, complexityScore: taskComplexity?.complexityScore };
}
/**
* Fetch a specific Task Master task by ID
* @param {string} taskId - Task ID
* @returns {Promise<Object>} Task object
*/
async fetchTaskMasterTaskById(taskId) {
const tasks = await this.fetchTaskMasterTasks(null, true);
if (!tasks) return null;
if (taskId.includes('.')) {
// This is a subtask ID (e.g., "5.2")
const [parentId, subtaskId] = taskId.split('.');
const parentTask = tasks.find(t => t.id === parseInt(parentId));
if (parentTask && parentTask.subtasks) {
const subtask = parentTask.subtasks.find(s => s.id === parseInt(subtaskId));
if (subtask) {
return { ...subtask, parentTask };
}
}
} else {
// This is a main task ID
return tasks.find(t => t.id === parseInt(taskId));
}
return null;
}
/**
* Analyze task complexity
* @param {Array<Object>} tasks - Tasks to analyze
* @returns {Promise<Array>} Complexity analysis results
*/
async analyzeTaskComplexity(tasks) {
log.info(`Analyzing complexity for ${tasks.length} tasks...`);
const toolName = 'analyze_complexity';
const requestUrl = `${TASKMASTER_MCP_URL}/${toolName}`;
const payload = {
parameters: {
projectRoot: this.projectRoot,
tasks: JSON.stringify(tasks),
threshold: '7' // Default threshold for high complexity
}
};
try {
const response = await fetch(requestUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
let errorData;
try {
errorData = await response.json();
} catch (parseError) {
errorData = await response.text();
}
log.error(`HTTP error analyzing task complexity: ${response.status} ${response.statusText}`);
log.error('Error details:', errorData);
throw new Error(`HTTP error ${response.status}`);
}
const responseData = await response.json();
if (responseData && responseData.success) {
log.info('Successfully analyzed task complexity');
return responseData.result.complexityAnalysis || [];
} else {
log.error('MCP Server indicated failure during complexity analysis:', responseData ? responseData.error : 'Unknown error structure');
return [];
}
} catch (error) {
log.error(`Error calling Task Master MCP server (${toolName}):`, error.message);
return [];
}
}
/**
* Execute a workflow step using the appropriate agent based on the action type
* @param {string} workflowId - Workflow ID
* @param {number} stepIndex - Step index
* @param {string} agentId - Agent ID
* @returns {Promise<Object>} Execution result
*/
async executeWorkflowStep(workflowId, stepIndex, agentId) {
const workflow = this.workflows.get(workflowId);
const agent = this.agents.get(agentId);
if (!workflow || !agent) {
throw new Error('Invalid workflow or agent ID');
}
const step = workflow.steps[stepIndex];
if (!step) {
throw new Error('Invalid step index');
}
// Get the action from the step
const action = step.action;
if (!action || !action.type) {
throw new Error('Step has no valid action');
}
// Execute the appropriate action based on the type
let result;
try {
switch (action.type) {
case 'createComponent':
result = await this.executeCreateComponent(agent, step, action.parameters);
break;
case 'implementApiLogic':
result = await this.executeImplementApiLogic(agent, step, action.parameters);
break;
case 'createTest':
result = await this.executeCreateTest(agent, step, action.parameters);
break;
case 'setupDatabase':
result = await this.executeSetupDatabase(agent, step, action.parameters);
break;
case 'configureAuthentication':
result = await this.executeConfigureAuthentication(agent, step, action.parameters);
break;
case 'implementBusinessLogic':
result = await this.executeImplementBusinessLogic(agent, step, action.parameters);
break;
case 'createUtility':
result = await this.executeCreateUtility(agent, step, action.parameters);
break;
default:
throw new Error(`Unsupported action type: ${action.type}`);
}
// Complete the step with the result
await this.completeStep(workflowId, stepIndex, result);
// Update the Task Master task status if taskMasterId is provided
if (step.taskMasterId) {
await this.setTaskMasterTaskStatus(step.taskMasterId, 'completed');
}
return result;
} catch (error) {
// Handle step failure
await this.handleStepFailure(workflowId, stepIndex, error);
// Update the Task Master task status if taskMasterId is provided
if (step.taskMasterId) {
await this.setTaskMasterTaskStatus(step.taskMasterId, 'failed');
}
throw error;
}
}
// Placeholder methods for executing different action types
// These would be implemented with actual agent execution logic
async executeCreateComponent(agent, step, parameters) {
log.info(`Agent ${agent.id} executing createComponent action for step ${step.id}`);
// Implementation would call the agent's createComponent method
return {
componentPath: `src/components/${parameters.componentName}.js`,
componentCode: '// Generated component code'
};
}
async executeImplementApiLogic(agent, step, parameters) {
log.info(`Agent ${agent.id} executing implementApiLogic action for step ${step.id}`);
// Implementation would call the agent's implementApiLogic method
return {
apiPath: `src/api/${parameters.endpointPath}.js`,
apiCode: '// Generated API code',
apiDocumentation: '# API Documentation'
};
}
async executeCreateTest(agent, step, parameters) {
log.info(`Agent ${agent.id} executing createTest action for step ${step.id}`);
// Check if the agent is a QA agent
if (agent.role === 'qa') {
// Agent is already a QA agent, use its createTest method
return await agent.createTest(parameters, {
taskId: step.taskMasterId,
workflowId: step.id,
testFramework: parameters.testFramework || 'jest',
runTests: parameters.runTests || false
});
} else {
// Create a temporary QA agent
log.info(`Creating temporary QA agent for step ${step.id}`);
const qaAgent = createQAAgent(`qa-${agent.id}`, {
capabilities: agent.capabilities,
knowledgeDomains: agent.knowledgeDomains
});
// Use the QA agent to create tests
return await qaAgent.createTest(parameters, {
taskId: step.taskMasterId,
workflowId: step.id,
testFramework: parameters.testFramework || 'jest',
runTests: parameters.runTests || false
});
}
}
async executeSetupDatabase(agent, step, parameters) {
log.info(`Agent ${agent.id} executing setupDatabase action for step ${step.id}`);
// Implementation would call the agent's setupDatabase method
return {
schemaPath: 'src/db/schema.js',
schemaCode: '// Generated schema code',
migrationPath: 'src/db/migrations/'
};
}
async executeConfigureAuthentication(agent, step, parameters) {
log.info(`Agent ${agent.id} executing configureAuthentication action for step ${step.id}`);
// Implementation would call the agent's configureAuthentication method
return {
authConfigPath: 'src/auth/config.js',
authImplementation: '// Generated auth implementation'
};
}
async executeImplementBusinessLogic(agent, step, parameters) {
log.info(`Agent ${agent.id} executing implementBusinessLogic action for step ${step.id}`);
// Implementation would call the agent's implementBusinessLogic method
return {
implementationPath: 'src/services/businessLogic.js',
implementationCode: '// Generated business logic code'
};
}
async executeCreateUtility(agent, step, parameters) {
log.info(`Agent ${agent.id} executing createUtility action for step ${step.id}`);
// Implementation would call the agent's createUtility method
return {
utilityPath: 'src/utils/utility.js',
utilityCode: '// Generated utility code',
utilityDocumentation: '# Utility Documentation'
};
}
async fetchTaskMasterTasks(statusFilter = null, withSubtasks = false) {
log.info(`Fetching Task Master tasks using fetch... Filter: ${statusFilter}, Subtasks: ${withSubtasks}`);
const toolName = 'get_tasks';
const requestUrl = `${TASKMASTER_MCP_URL}/${toolName}`;
const payload = {
parameters: {
projectRoot: this.projectRoot,
status: statusFilter,
withSubtasks: withSubtasks
}
// context: { session: { ... } } // Add context if needed
};
try {
// Use fetch instead of axios
const response = await fetch(requestUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// TODO: Add authentication headers if required
},
body: JSON.stringify(payload)
});
if (!response.ok) {
// Handle non-2xx responses
let errorData;
try {
errorData = await response.json();
} catch (parseError) {
errorData = await response.text(); // Fallback to text if JSON parsing fails
}
log.error(`HTTP error fetching tasks: ${response.status} ${response.statusText}`);
log.error('Error details:', errorData);
throw new Error(`HTTP error ${response.status}`);
}
const responseData = await response.json();
if (responseData && responseData.success) {
log.info('Successfully fetched tasks from Task Master via fetch:');
// Optional: console.log(JSON.stringify(responseData.result, null, 2));
return responseData.result; // Return the actual task data
} else {
log.error('MCP Server indicated failure:', responseData ? responseData.error : 'Unknown error structure');
return null;
}
} catch (error) {
// Catch network errors or errors thrown from response handling
log.error(`Error calling Task Master MCP server (${toolName}) with fetch:`, error.message);
return null;
}
}
async expandTaskMasterTask(taskId, options = {}) {
const { num, research, prompt, force } = options;
log.info(`Expanding Task Master task ${taskId} using fetch...`);
const toolName = 'expand_task';
const requestUrl = `${TASKMASTER_MCP_URL}/${toolName}`;
const payload = {
parameters: {
projectRoot: this.projectRoot,
id: taskId,
// Optional parameters for expand_task
...(num && { num: String(num) }), // Ensure num is string if provided
...(research && { research }),
...(prompt && { prompt }),
...(force && { force })
}
// context: { session: { ... } } // Add context if needed
};
try {
const response = await fetch(requestUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// TODO: Add authentication headers if required
},
body: JSON.stringify(payload)
});
if (!response.ok) {
let errorData;
try {
errorData = await response.json();
} catch (parseError) {
errorData = await response.text();
}
log.error(`HTTP error expanding task ${taskId}: ${response.status} ${response.statusText}`);
log.error('Error details:', errorData);
throw new Error(`HTTP error ${response.status}`);
}
const responseData = await response.json();
if (responseData && responseData.success) {
log.info(`Successfully requested expansion for task ${taskId}.`);
// expand_task usually returns the updated task data including subtasks
return responseData.result;
} else {
log.error(`MCP Server indicated failure during expansion of task ${taskId}:`, responseData ? responseData.error : 'Unknown error structure');
return null;
}
} catch (error) {
log.error(`Error calling Task Master MCP server (${toolName}) for task ${taskId}:`, error.message);
return null;
}
}
async setTaskMasterTaskStatus(taskId, status) {
// taskId can be a main task ID ('5') or a subtask ID ('5.2')
log.info(`Setting Task Master task ${taskId} status to '${status}' using fetch...`);
const toolName = 'set_task_status';
const requestUrl = `${TASKMASTER_MCP_URL}/${toolName}`;
const payload = {
parameters: {
projectRoot: this.projectRoot,
id: String(taskId), // Ensure ID is a string
status: status
}
// context: { session: { ... } } // Add context if needed
};
try {
const response = await fetch(requestUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// TODO: Add authentication headers if required
},
body: JSON.stringify(payload)
});
if (!response.ok) {
let errorData;
try {
errorData = await response.json();
} catch (parseError) {
errorData = await response.text();
}
log.error(`HTTP error setting status for task ${taskId}: ${response.status} ${response.statusText}`);
log.error('Error details:', errorData);
throw new Error(`HTTP error ${response.status}`);
}
const responseData = await response.json();
if (responseData && responseData.success) {
log.info(`Successfully set status for task ${taskId} to ${status}.`);
return responseData.result; // Usually confirms the update
} else {
log.error(`MCP Server indicated failure setting status for task ${taskId}:`, responseData ? responseData.error : 'Unknown error structure');
return null;
}
} catch (error) {
log.error(`Error calling Task Master MCP server (${toolName}) for task ${taskId}:`, error.message);
return null;
}
}
async fetchTaskMasterTaskById(taskId) {
log.info(`Fetching details for Task Master task ${taskId} using fetch...`);
const toolName = 'get_task'; // Assumes this MCP tool exists
const requestUrl = `${TASKMASTER_MCP_URL}/${toolName}`;
const payload = {
parameters: {
projectRoot: this.projectRoot,
id: String(taskId)
}
// context: { session: { ... } } // Add context if needed
};
try {
const response = await fetch(requestUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// TODO: Add authentication headers if required
},
body: JSON.stringify(payload)
});
if (!response.ok) {
let errorData;
try {
errorData = await response.json();
} catch (parseError) {
errorData = await response.text();
}
log.error(`HTTP error fetching task ${taskId}: ${response.status} ${response.statusText}`);
log.error('Error details:', errorData);
throw new Error(`HTTP error ${response.status}`);
}
const responseData = await response.json();
if (responseData && responseData.success) {
log.info(`Successfully fetched details for task ${taskId}.`);
return responseData.result; // Return the task details
} else {
log.error(`MCP Server indicated failure fetching task ${taskId}:`, responseData ? responseData.error : 'Unknown error structure');
return null;
}
} catch (error) {
log.error(`Error calling Task Master MCP server (${toolName}) for task ${taskId}:`, error.message);
return null;
}
}
}