vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
915 lines (914 loc) • 40.1 kB
JavaScript
import { DecompositionService } from '../../services/decomposition-service.js';
import { getTaskOperations } from '../../core/operations/task-operations.js';
import { getProjectOperations } from '../../core/operations/project-operations.js';
import { getEpicService } from '../../services/epic-service.js';
import { ProjectAnalyzer } from '../../utils/project-analyzer.js';
import { getPathResolver } from '../../utils/path-resolver.js';
import { getVibeTaskManagerConfig } from '../../utils/config-loader.js';
import logger from '../../../../logger.js';
async function resolveEpicIdForTask(partialTask) {
try {
if (partialTask.epicId && partialTask.epicId !== 'default-epic') {
return partialTask.epicId;
}
const { getEpicContextResolver } = await import('../../services/epic-context-resolver.js');
const contextResolver = getEpicContextResolver();
const taskContext = partialTask.title && partialTask.description ? {
title: partialTask.title,
description: partialTask.description,
type: partialTask.type || 'development',
tags: partialTask.tags || []
} : undefined;
const resolverParams = {
projectId: partialTask.projectId || 'default-project',
taskContext
};
const contextResult = await contextResolver.resolveEpicContext(resolverParams);
return contextResult.epicId;
}
catch (error) {
logger.warn({ err: error, partialTask }, 'Failed to resolve epic ID for task, using fallback');
return `${partialTask.projectId || 'default-project'}-main-epic`;
}
}
async function resolveEpicIdForProject(projectId, projectName) {
try {
const { getEpicContextResolver } = await import('../../services/epic-context-resolver.js');
const contextResolver = getEpicContextResolver();
const taskContext = {
title: `Complete ${projectName}`,
description: `Project implementation for ${projectName}`,
type: 'development',
tags: ['project-decomposition']
};
const resolverParams = {
projectId,
taskContext
};
const contextResult = await contextResolver.resolveEpicContext(resolverParams);
return contextResult.epicId;
}
catch (error) {
logger.warn({ err: error, projectId, projectName }, 'Failed to resolve epic ID for project, using fallback');
return `${projectId}-main-epic`;
}
}
async function createCompleteAtomicTask(partialTask) {
const now = new Date();
return {
id: partialTask.id,
title: partialTask.title,
description: partialTask.description,
status: partialTask.status || 'pending',
priority: partialTask.priority || 'medium',
type: partialTask.type || 'development',
functionalArea: partialTask.functionalArea || 'data-management',
estimatedHours: partialTask.estimatedHours || 4,
actualHours: partialTask.actualHours,
epicId: await resolveEpicIdForTask(partialTask),
projectId: partialTask.projectId || 'default-project',
dependencies: partialTask.dependencies || [],
dependents: partialTask.dependents || [],
filePaths: partialTask.filePaths || [],
acceptanceCriteria: partialTask.acceptanceCriteria || [],
testingRequirements: partialTask.testingRequirements || {
unitTests: [],
integrationTests: [],
performanceTests: [],
coverageTarget: 80
},
performanceCriteria: partialTask.performanceCriteria || {},
qualityCriteria: partialTask.qualityCriteria || {
codeQuality: [],
documentation: [],
typeScript: true,
eslint: true
},
integrationCriteria: partialTask.integrationCriteria || {
compatibility: [],
patterns: []
},
validationMethods: partialTask.validationMethods || {
automated: [],
manual: []
},
assignedAgent: partialTask.assignedAgent,
executionContext: partialTask.executionContext,
createdAt: partialTask.createdAt || now,
updatedAt: partialTask.updatedAt || now,
startedAt: partialTask.startedAt,
completedAt: partialTask.completedAt,
createdBy: partialTask.createdBy || 'system',
tags: partialTask.tags || [],
metadata: partialTask.metadata || {
createdAt: now,
updatedAt: now,
createdBy: 'system',
tags: []
}
};
}
export class DecomposeTaskHandler {
intent = 'decompose_task';
resolveProjectPath(context) {
const pathResolver = getPathResolver();
return pathResolver.resolveProjectPathFromContext(context);
}
async handle(recognizedIntent, toolParams, context) {
try {
logger.info({
intent: recognizedIntent.intent,
sessionId: context.sessionId
}, 'Processing task decomposition request');
const taskId = this.extractTaskId(recognizedIntent, toolParams);
const additionalContext = this.extractAdditionalContext(recognizedIntent, toolParams);
const options = this.extractDecompositionOptions(recognizedIntent, toolParams);
if (!taskId) {
return {
success: false,
result: {
content: [{
type: "text",
text: "❌ Please specify a task ID to decompose. For example: 'decompose task T001' or 'break down the authentication task'"
}],
isError: true
}
};
}
const taskOperations = getTaskOperations();
const taskResult = await taskOperations.getTask(taskId);
if (!taskResult.success) {
return {
success: false,
result: {
content: [{
type: "text",
text: `❌ Task ${taskId} not found. Please check the task ID and try again.`
}],
isError: true
}
};
}
const task = taskResult.data;
const decompositionService = new DecompositionService(context.config);
const projectAnalyzer = ProjectAnalyzer.getInstance();
const projectPath = this.resolveProjectPath(context);
let languages;
let frameworks;
let tools;
try {
languages = await projectAnalyzer.detectProjectLanguages(projectPath);
}
catch (error) {
logger.warn({ error, projectPath }, 'Language detection failed, using fallback');
languages = ['javascript'];
}
try {
frameworks = await projectAnalyzer.detectProjectFrameworks(projectPath);
}
catch (error) {
logger.warn({ error, projectPath }, 'Framework detection failed, using fallback');
frameworks = ['node.js'];
}
try {
tools = await projectAnalyzer.detectProjectTools(projectPath);
}
catch (error) {
logger.warn({ error, projectPath }, 'Tools detection failed, using fallback');
tools = ['git', 'npm'];
}
const decompositionRequest = {
task: await createCompleteAtomicTask({
id: task.id,
title: task.title,
description: additionalContext || task.description,
type: task.type,
priority: task.priority,
estimatedHours: task.estimatedHours,
acceptanceCriteria: task.acceptanceCriteria,
tags: task.tags,
filePaths: task.filePaths || [],
projectId: task.projectId,
epicId: task.epicId,
status: task.status,
createdBy: task.createdBy,
createdAt: task.createdAt,
updatedAt: task.updatedAt
}),
context: {
projectId: task.projectId,
projectPath: process.cwd(),
projectName: task.projectId,
description: `Task decomposition context for ${task.title}`,
languages,
frameworks,
buildTools: [],
tools,
configFiles: [],
entryPoints: [],
architecturalPatterns: [],
existingTasks: [],
codebaseSize: 'medium',
teamSize: 1,
complexity: 'medium',
codebaseContext: {
relevantFiles: [],
contextSummary: `Task decomposition context for ${task.title}`,
gatheringMetrics: {
searchTime: 0,
readTime: 0,
scoringTime: 0,
totalTime: 0,
cacheHitRate: 0
},
totalContextSize: 0,
averageRelevance: 0
},
structure: {
sourceDirectories: ['src'],
testDirectories: ['test', 'tests', '__tests__'],
docDirectories: ['docs', 'documentation'],
buildDirectories: ['dist', 'build', 'lib']
},
dependencies: {
production: [],
development: [],
external: []
},
metadata: {
createdAt: new Date(),
updatedAt: new Date(),
version: '1.0.0',
source: 'manual'
}
},
sessionId: `nl-decompose-${context.sessionId}`,
options: {
maxDepth: options.maxDepth || 3,
minHours: options.minHours || 0.5,
maxHours: options.maxHours || 8,
forceDecomposition: options.force || false
}
};
const session = await decompositionService.startDecomposition(decompositionRequest);
const timeout = 30000;
const startTime = Date.now();
while ((session.status === 'pending' || session.status === 'in_progress') && (Date.now() - startTime) < timeout) {
await new Promise(resolve => setTimeout(resolve, 1000));
const updatedSession = decompositionService.getSession(session.id);
if (updatedSession) {
Object.assign(session, updatedSession);
}
}
if (session.status === 'pending' || session.status === 'in_progress') {
return {
success: true,
result: {
content: [{
type: "text",
text: `⏳ Task decomposition is in progress for "${task.title}". This may take a few moments. Session ID: ${session.id}`
}]
}
};
}
if (session.status === 'failed') {
return {
success: false,
result: {
content: [{
type: "text",
text: `❌ Task decomposition failed: ${session.error || 'Unknown error'}`
}],
isError: true
}
};
}
if (session.results && session.results.length > 0 && session.results[0].subTasks.length > 0) {
const decomposedTasks = session.results[0].subTasks;
const totalHours = decomposedTasks.reduce((sum, task) => sum + task.estimatedHours, 0);
let responseText = `✅ Successfully decomposed "${task.title}" into ${decomposedTasks.length} atomic tasks:\n\n`;
decomposedTasks.forEach((atomicTask, index) => {
responseText += `${index + 1}. **${atomicTask.title}** (${atomicTask.estimatedHours}h)\n`;
responseText += ` - Type: ${atomicTask.type}, Priority: ${atomicTask.priority}\n`;
responseText += ` - ID: ${atomicTask.id}\n`;
if (atomicTask.filePaths && atomicTask.filePaths.length > 0) {
responseText += ` - Files: ${atomicTask.filePaths.slice(0, 3).join(', ')}${atomicTask.filePaths.length > 3 ? '...' : ''}\n`;
}
responseText += '\n';
});
responseText += `📊 **Summary:**\n`;
responseText += `- Total estimated hours: ${totalHours}\n`;
responseText += `- Average task size: ${(totalHours / decomposedTasks.length).toFixed(1)} hours\n`;
return {
success: true,
result: {
content: [{
type: "text",
text: responseText
}]
},
followUpSuggestions: [
`List all tasks for project ${task.projectId}`,
`Show details for task ${decomposedTasks[0]?.id}`,
'Create a new task'
]
};
}
else {
return {
success: true,
result: {
content: [{
type: "text",
text: `ℹ️ Task "${task.title}" is already atomic and doesn't need further decomposition.`
}]
}
};
}
}
catch (error) {
logger.error({
err: error,
intent: recognizedIntent.intent,
sessionId: context.sessionId
}, 'Task decomposition failed');
return {
success: false,
result: {
content: [{
type: "text",
text: `❌ Failed to decompose task: ${error instanceof Error ? error.message : 'Unknown error'}`
}],
isError: true
}
};
}
}
extractTaskId(recognizedIntent, toolParams) {
if (toolParams.taskId) {
return toolParams.taskId;
}
const taskEntity = recognizedIntent.entities.find(e => e.type === 'taskId');
if (taskEntity) {
return taskEntity.value;
}
const input = recognizedIntent.originalInput.toLowerCase();
const taskIdMatch = input.match(/\b(t\d+|task[-_]?\d+|[a-z]+-\d+)\b/i);
if (taskIdMatch) {
return taskIdMatch[1].toUpperCase();
}
const taskNameMatch = input.match(/(?:the\s+)?(\w+)\s+task/i);
if (taskNameMatch) {
return taskNameMatch[1];
}
return null;
}
extractAdditionalContext(recognizedIntent, toolParams) {
if (toolParams.description || toolParams.context) {
return (toolParams.description || toolParams.context);
}
const input = recognizedIntent.originalInput;
const contextPhrases = [
/with\s+focus\s+on\s+(.+)/i,
/considering\s+(.+)/i,
/taking\s+into\s+account\s+(.+)/i,
/for\s+(.+)/i
];
for (const phrase of contextPhrases) {
const match = input.match(phrase);
if (match) {
return match[1].trim();
}
}
return null;
}
extractDecompositionOptions(recognizedIntent, _toolParams) {
const options = {};
if (recognizedIntent.originalInput.toLowerCase().includes('force') ||
recognizedIntent.originalInput.toLowerCase().includes('anyway')) {
options.force = true;
}
const input = recognizedIntent.originalInput.toLowerCase();
if (input.includes('small') || input.includes('tiny')) {
options.maxHours = 4;
}
else if (input.includes('large') || input.includes('big')) {
options.maxHours = 12;
}
const hoursMatch = input.match(/(\d+)\s*hours?/);
if (hoursMatch) {
options.maxHours = parseInt(hoursMatch[1], 10);
}
return options;
}
}
export class DecomposeEpicHandler {
intent = 'decompose_epic';
resolveProjectPath(context) {
const pathResolver = getPathResolver();
return pathResolver.resolveProjectPathFromContext(context);
}
async handle(recognizedIntent, toolParams, context) {
try {
logger.info({
intent: recognizedIntent.intent,
sessionId: context.sessionId
}, 'Processing epic decomposition request');
const epicId = this.extractEpicId(recognizedIntent, toolParams);
const additionalContext = this.extractAdditionalContext(recognizedIntent, toolParams);
const options = await this.extractDecompositionOptions(recognizedIntent, toolParams);
if (!epicId) {
return {
success: false,
result: {
content: [{
type: "text",
text: "❌ Please specify an epic ID to decompose. For example: 'decompose epic E001' or 'break down the authentication epic'"
}],
isError: true
}
};
}
const epicService = getEpicService();
const epicResult = await epicService.getEpic(epicId);
if (!epicResult.success) {
return {
success: false,
result: {
content: [{
type: "text",
text: `❌ Epic ${epicId} not found. Please check the epic ID and try again.`
}],
isError: true
}
};
}
const epic = epicResult.data;
const decompositionService = new DecompositionService(context.config);
const projectAnalyzer = ProjectAnalyzer.getInstance();
const projectPath = this.resolveProjectPath(context);
let languages;
let frameworks;
let tools;
try {
languages = await projectAnalyzer.detectProjectLanguages(projectPath);
}
catch (error) {
logger.warn({ error, projectPath }, 'Language detection failed, using fallback');
languages = ['javascript'];
}
try {
frameworks = await projectAnalyzer.detectProjectFrameworks(projectPath);
}
catch (error) {
logger.warn({ error, projectPath }, 'Framework detection failed, using fallback');
frameworks = ['node.js'];
}
try {
tools = await projectAnalyzer.detectProjectTools(projectPath);
}
catch (error) {
logger.warn({ error, projectPath }, 'Tools detection failed, using fallback');
tools = ['git', 'npm'];
}
const epicAsTask = await createCompleteAtomicTask({
id: `epic-decomp-${epic.id}`,
title: `Decompose Epic: ${epic.title}`,
description: additionalContext || epic.description || `Decompose epic ${epic.title} into actionable tasks`,
type: 'development',
priority: epic.priority || 'medium',
estimatedHours: epic.estimatedHours || 8,
acceptanceCriteria: [`Epic ${epic.title} is fully decomposed into actionable tasks`],
tags: [...(epic.metadata?.tags || []), 'epic-decomposition'],
filePaths: [],
projectId: epic.projectId,
epicId: epic.id,
status: 'pending',
createdBy: context.sessionId,
createdAt: new Date(),
updatedAt: new Date()
});
const decompositionRequest = {
task: epicAsTask,
context: {
projectId: epic.projectId,
projectPath: process.cwd(),
projectName: epic.projectId,
description: `Epic decomposition context for ${epic.title}`,
languages,
frameworks,
buildTools: [],
tools,
configFiles: [],
entryPoints: [],
architecturalPatterns: [],
existingTasks: [],
codebaseSize: 'medium',
teamSize: 1,
complexity: 'medium',
codebaseContext: {
relevantFiles: [],
contextSummary: `Epic decomposition context for ${epic.title}`,
gatheringMetrics: {
searchTime: 0,
readTime: 0,
scoringTime: 0,
totalTime: 0,
cacheHitRate: 0
},
totalContextSize: 0,
averageRelevance: 0
},
structure: {
sourceDirectories: ['src'],
testDirectories: ['test', 'tests', '__tests__'],
docDirectories: ['docs', 'documentation'],
buildDirectories: ['dist', 'build', 'lib']
},
dependencies: {
production: [],
development: [],
external: []
},
metadata: {
createdAt: new Date(),
updatedAt: new Date(),
version: '1.0.0',
source: 'manual'
}
},
sessionId: `nl-epic-decompose-${context.sessionId}`,
options: {
maxDepth: options.maxDepth || 2,
minHours: options.minHours || 0.1,
maxHours: options.maxHours || 4,
forceDecomposition: options.force || true
}
};
const session = await decompositionService.startDecomposition(decompositionRequest);
const timeout = 30000;
const startTime = Date.now();
while ((session.status === 'pending' || session.status === 'in_progress') && (Date.now() - startTime) < timeout) {
await new Promise(resolve => setTimeout(resolve, 1000));
const updatedSession = decompositionService.getSession(session.id);
if (updatedSession) {
Object.assign(session, updatedSession);
}
}
if (session.status === 'pending' || session.status === 'in_progress') {
return {
success: true,
result: {
content: [{
type: "text",
text: `⏳ Epic decomposition is in progress for "${epic.title}". This may take a few moments. Session ID: ${session.id}`
}]
}
};
}
if (session.status === 'failed') {
return {
success: false,
result: {
content: [{
type: "text",
text: `❌ Epic decomposition failed for "${epic.title}". Error: ${session.error || 'Unknown error'}`
}],
isError: true
}
};
}
const taskCount = session.persistedTasks?.length || 0;
const successMessage = `✅ Successfully decomposed epic "${epic.title}" into ${taskCount} actionable tasks.`;
const tasksList = session.persistedTasks?.map(task => ` • ${task.title} (${task.id})`).join('\n') || '';
return {
success: true,
result: {
content: [{
type: "text",
text: `${successMessage}\n\nGenerated Tasks:\n${tasksList}\n\nSession ID: ${session.id}`
}]
}
};
}
catch (error) {
logger.error({ err: error }, 'Epic decomposition failed');
return {
success: false,
result: {
content: [{
type: "text",
text: `❌ Epic decomposition failed: ${error instanceof Error ? error.message : 'Unknown error'}`
}],
isError: true
}
};
}
}
extractEpicId(recognizedIntent, toolParams) {
if (toolParams.epicId) {
return toolParams.epicId;
}
const epicEntity = recognizedIntent.entities.find(e => e.type === 'epicId');
if (epicEntity) {
return epicEntity.value;
}
const input = recognizedIntent.originalInput.toLowerCase();
const epicIdMatch = input.match(/\b(e\d+|epic[-_]?\d+|[a-z]+-\d+)\b/i);
if (epicIdMatch) {
return epicIdMatch[1].toUpperCase();
}
const epicNameMatch = input.match(/(?:the\s+)?(\w+)\s+epic/i);
if (epicNameMatch) {
return epicNameMatch[1];
}
return null;
}
extractAdditionalContext(recognizedIntent, toolParams) {
if (toolParams.description || toolParams.context) {
return (toolParams.description || toolParams.context);
}
const input = recognizedIntent.originalInput;
const contextPhrases = [
/with\s+focus\s+on\s+(.+)/i,
/considering\s+(.+)/i,
/taking\s+into\s+account\s+(.+)/i,
/for\s+(.+)/i
];
for (const phrase of contextPhrases) {
const match = input.match(phrase);
if (match) {
return match[1].trim();
}
}
return null;
}
async extractDecompositionOptions(recognizedIntent, toolParams) {
const config = await getVibeTaskManagerConfig();
const rddConfig = config?.taskManager?.rddConfig;
const timeouts = config?.taskManager?.timeouts;
const retryPolicy = config?.taskManager?.retryPolicy;
if (!config) {
throw new Error('Centralized configuration not available for epic decomposition');
}
const options = {
maxDepth: rddConfig?.maxDepth ?? config.taskManager.rddConfig.maxDepth,
maxSubTasks: rddConfig?.maxSubTasks ?? config.taskManager.rddConfig.maxSubTasks,
minConfidence: rddConfig?.minConfidence ?? config.taskManager.rddConfig.minConfidence,
enableParallelDecomposition: rddConfig?.enableParallelDecomposition ?? config.taskManager.rddConfig.enableParallelDecomposition,
epicTimeLimit: rddConfig?.epicTimeLimit ?? config.taskManager.rddConfig.epicTimeLimit,
minHours: 0.1,
maxHours: Math.floor((rddConfig?.epicTimeLimit ?? config.taskManager.rddConfig.epicTimeLimit) / 8),
force: true,
timeouts: {
taskDecomposition: timeouts?.taskDecomposition ?? config.taskManager.timeouts.taskDecomposition,
recursiveTaskDecomposition: timeouts?.recursiveTaskDecomposition ?? config.taskManager.timeouts.recursiveTaskDecomposition,
llmRequest: timeouts?.llmRequest ?? config.taskManager.timeouts.llmRequest
},
retryPolicy: {
maxRetries: retryPolicy?.maxRetries ?? config.taskManager.retryPolicy.maxRetries,
backoffMultiplier: retryPolicy?.backoffMultiplier ?? config.taskManager.retryPolicy.backoffMultiplier,
initialDelayMs: retryPolicy?.initialDelayMs ?? config.taskManager.retryPolicy.initialDelayMs,
maxDelayMs: retryPolicy?.maxDelayMs ?? config.taskManager.retryPolicy.maxDelayMs,
enableExponentialBackoff: retryPolicy?.enableExponentialBackoff ?? config.taskManager.retryPolicy.enableExponentialBackoff
}
};
if (toolParams.options && typeof toolParams.options === 'object') {
const toolOptions = toolParams.options;
Object.assign(options, toolOptions);
}
const input = recognizedIntent.originalInput.toLowerCase();
const depthMatch = input.match(/(\d+)\s*levels?/);
if (depthMatch) {
const extractedDepth = parseInt(depthMatch[1], 10);
if (extractedDepth > 0 && extractedDepth <= 5) {
options.maxDepth = extractedDepth;
}
}
if (input.includes('parallel') || input.includes('concurrent') || input.includes('simultaneously')) {
options.enableParallelDecomposition = true;
}
else if (input.includes('sequential') || input.includes('one by one') || input.includes('step by step')) {
options.enableParallelDecomposition = false;
}
return options;
}
}
export class DecomposeProjectHandler {
intent = 'decompose_project';
resolveProjectPath(context) {
const pathResolver = getPathResolver();
return pathResolver.resolveProjectPathFromContext(context);
}
async handle(recognizedIntent, toolParams, context) {
try {
logger.info({
intent: recognizedIntent.intent,
sessionId: context.sessionId
}, 'Processing project decomposition request');
const projectId = this.extractProjectId(recognizedIntent, toolParams);
const additionalContext = this.extractAdditionalContext(recognizedIntent, toolParams);
if (!projectId) {
return {
success: false,
result: {
content: [{
type: "text",
text: "❌ Please specify a project ID or name to decompose. For example: 'decompose project PID-WEBAPP-001' or 'break down the web app project'"
}],
isError: true
}
};
}
const projectOperations = getProjectOperations();
const projectResult = await projectOperations.getProject(projectId);
if (!projectResult.success) {
return {
success: false,
result: {
content: [{
type: "text",
text: `❌ Project ${projectId} not found. Please check the project ID and try again.`
}],
isError: true
}
};
}
const project = projectResult.data;
const decompositionService = new DecompositionService(context.config);
const projectTask = await createCompleteAtomicTask({
id: `project-${project.id}`,
title: `Complete ${project.name}`,
description: additionalContext || project.description,
type: 'development',
priority: 'high',
estimatedHours: 120,
acceptanceCriteria: [`Project ${project.name} should be fully implemented and tested`],
tags: ['project-decomposition', ...project.metadata.tags],
filePaths: [],
projectId: project.id,
epicId: await resolveEpicIdForProject(project.id, project.name),
createdBy: 'system'
});
const projectAnalyzer = ProjectAnalyzer.getInstance();
const projectPath = this.resolveProjectPath(context);
let languages;
let frameworks;
let tools;
try {
languages = project.techStack.languages?.length
? project.techStack.languages
: await projectAnalyzer.detectProjectLanguages(projectPath);
}
catch (error) {
logger.warn({ error, projectPath }, 'Language detection failed for project, using fallback');
languages = ['typescript'];
}
try {
frameworks = project.techStack.frameworks?.length
? project.techStack.frameworks
: await projectAnalyzer.detectProjectFrameworks(projectPath);
}
catch (error) {
logger.warn({ error, projectPath }, 'Framework detection failed for project, using fallback');
frameworks = ['node.js'];
}
try {
tools = project.techStack.tools?.length
? project.techStack.tools
: await projectAnalyzer.detectProjectTools(projectPath);
}
catch (error) {
logger.warn({ error, projectPath }, 'Tools detection failed for project, using fallback');
tools = ['vscode', 'git'];
}
const decompositionRequest = {
task: projectTask,
context: {
projectId: project.id,
projectPath: this.resolveProjectPath(context),
projectName: project.name,
description: additionalContext || project.description,
languages,
frameworks,
buildTools: [],
tools,
configFiles: [],
entryPoints: [],
architecturalPatterns: [],
existingTasks: [],
codebaseSize: 'large',
teamSize: 1,
complexity: 'high',
codebaseContext: {
relevantFiles: [],
contextSummary: additionalContext || project.description,
gatheringMetrics: {
searchTime: 0,
readTime: 0,
scoringTime: 0,
totalTime: 0,
cacheHitRate: 0
},
totalContextSize: 0,
averageRelevance: 0
},
structure: {
sourceDirectories: ['src'],
testDirectories: ['test', 'tests', '__tests__'],
docDirectories: ['docs', 'documentation'],
buildDirectories: ['dist', 'build', 'lib']
},
dependencies: {
production: [],
development: [],
external: []
},
metadata: {
createdAt: new Date(),
updatedAt: new Date(),
version: '1.0.0',
source: 'manual'
}
},
sessionId: `nl-project-decompose-${context.sessionId}`,
options: {
maxDepth: 2,
minHours: 1,
maxHours: 8,
forceDecomposition: true
}
};
const session = await decompositionService.startDecomposition(decompositionRequest);
return {
success: true,
result: {
content: [{
type: "text",
text: `🚀 Started decomposition of project "${project.name}". This will break down the project into manageable epics and tasks. Session ID: ${session.id}\n\nThis process may take a few moments as we analyze the project scope and create a comprehensive breakdown.`
}]
},
followUpSuggestions: [
`Check decomposition status for session ${session.id}`,
`List all projects`,
`Show project details for ${project.id}`
]
};
}
catch (error) {
logger.error({
err: error,
intent: recognizedIntent.intent,
sessionId: context.sessionId
}, 'Project decomposition failed');
return {
success: false,
result: {
content: [{
type: "text",
text: `❌ Failed to decompose project: ${error instanceof Error ? error.message : 'Unknown error'}`
}],
isError: true
}
};
}
}
extractProjectId(recognizedIntent, toolParams) {
if (toolParams.projectId || toolParams.projectName) {
return (toolParams.projectId || toolParams.projectName);
}
const projectEntity = recognizedIntent.entities.find(e => e.type === 'projectId' || e.type === 'projectName');
if (projectEntity) {
return projectEntity.value;
}
const input = recognizedIntent.originalInput;
const projectIdMatch = input.match(/\b(pid[-_]?\w+[-_]?\d+)\b/i);
if (projectIdMatch) {
return projectIdMatch[1].toUpperCase();
}
const projectNameMatch = input.match(/(?:the\s+)?(.+?)\s+project/i);
if (projectNameMatch) {
return projectNameMatch[1].trim();
}
return null;
}
extractAdditionalContext(recognizedIntent, toolParams) {
if (toolParams.description || toolParams.context) {
return (toolParams.description || toolParams.context);
}
const input = recognizedIntent.originalInput;
const contextPhrases = [
/with\s+focus\s+on\s+(.+)/i,
/considering\s+(.+)/i,
/for\s+(.+)/i,
/including\s+(.+)/i
];
for (const phrase of contextPhrases) {
const match = input.match(phrase);
if (match) {
return match[1].trim();
}
}
return null;
}
}