UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

1,070 lines (1,055 loc) 50.3 kB
import { getStorageManager } from '../core/storage/storage-manager.js'; import { getProjectOperations } from '../core/operations/project-operations.js'; import { getEpicService } from './epic-service.js'; import { PRDIntegrationService } from '../integrations/prd-integration.js'; import logger from '../../../logger.js'; export class EpicContextResolver { static instance; constructor() { } static getInstance() { if (!EpicContextResolver.instance) { EpicContextResolver.instance = new EpicContextResolver(); } return EpicContextResolver.instance; } async resolveEpicContext(params) { try { const functionalArea = params.functionalArea || await this.extractFunctionalArea(params.taskContext, params.projectId, params.config); logger.debug({ projectId: params.projectId, functionalArea: params.functionalArea, extractedFunctionalArea: functionalArea, taskTitle: params.taskContext?.title }, 'Resolving epic context'); const existingEpic = await this.findExistingEpic(params); if (existingEpic) { logger.debug({ epicId: existingEpic.epicId, source: existingEpic.source }, 'Found existing epic'); return existingEpic; } logger.debug({ functionalArea }, 'No existing epic found, attempting to create functional area epic'); const createdEpic = await this.createFunctionalAreaEpic(params); if (createdEpic) { logger.debug({ epicId: createdEpic.epicId, functionalArea }, 'Created new functional area epic'); return createdEpic; } logger.debug('No functional area epic created, falling back to main epic'); const fallbackEpic = await this.createMainEpic(params); return fallbackEpic; } catch (error) { logger.warn({ err: error, projectId: params.projectId }, 'Epic context resolution failed, attempting fallback epic creation'); try { const fallbackEpic = await this.createProjectSpecificFallbackEpic(params); return fallbackEpic; } catch (fallbackError) { logger.error({ err: fallbackError, projectId: params.projectId }, 'Fallback epic creation also failed'); return { epicId: `${params.projectId}-emergency-epic`, epicName: 'Emergency Epic', source: 'fallback', confidence: 0.1, created: false }; } } } async extractFunctionalArea(taskContext, projectId, config) { const startTime = Date.now(); if (projectId && config) { try { const prdResult = await this.extractFunctionalAreaFromPRDWithRetries(projectId, config, taskContext); if (prdResult) { logger.info({ projectId, functionalArea: prdResult, strategy: 'enhanced_prd_llm', duration: Date.now() - startTime }, 'Successfully extracted functional area using enhanced LLM-powered PRD analysis'); return prdResult; } logger.debug({ projectId }, 'Enhanced PRD analysis completed but no functional areas extracted'); } catch (error) { logger.warn({ err: error, projectId }, 'Enhanced LLM PRD analysis failed, falling back to standard functional areas'); } } const standardArea = this.extractFromStandardFunctionalAreas(taskContext); if (standardArea) { logger.debug({ projectId, functionalArea: standardArea, strategy: 'standard_areas', duration: Date.now() - startTime }, 'Extracted functional area using standard 11 functional areas'); } else { logger.debug({ projectId, strategy: 'none', duration: Date.now() - startTime }, 'No functional area could be extracted using any strategy'); } return standardArea; } async extractFunctionalAreaFromPRDWithRetries(projectId, config, taskContext, maxRetries = 3) { let attemptCount = 0; let lastError = null; let validationFeedback = ''; while (attemptCount < maxRetries) { try { attemptCount++; logger.debug({ projectId, attemptCount, maxRetries }, 'Attempting enhanced PRD functional area extraction'); const prdService = PRDIntegrationService.getInstance(); const prdInfo = await prdService.detectExistingPRD(projectId); if (!prdInfo) { logger.debug({ projectId }, 'No PRD found for project, cannot extract functional areas'); return null; } const prdResult = await prdService.parsePRD(prdInfo.filePath); if (!prdResult.success || !prdResult.prdData) { logger.debug({ projectId }, 'Failed to parse PRD file, cannot extract functional areas'); return null; } const enhancedPrompt = this.buildEnhancedPRDExtractionPrompt(prdResult.prdData, taskContext, validationFeedback); const { performFormatAwareLlmCall } = await import('../../../utils/llmHelper.js'); const llmResult = await performFormatAwareLlmCall(enhancedPrompt, 'You are an expert software architect analyzing PRD content to extract project-specific functional areas with high accuracy.', config, 'enhanced_prd_analysis', 'json'); const extractedAreas = this.parseAndValidateLLMFunctionalAreas(llmResult); if (extractedAreas.length > 0) { const primaryArea = extractedAreas[0]; logger.info({ projectId, attemptCount, extractedAreas, primaryArea }, 'Successfully extracted and validated functional area from PRD'); return primaryArea; } validationFeedback = this.generateValidationFeedback(llmResult, attemptCount); logger.warn({ projectId, attemptCount, validationFeedback }, 'PRD extraction attempt failed validation, preparing retry'); } catch (error) { lastError = error; validationFeedback = `Previous attempt ${attemptCount} failed with error: ${lastError.message}. Please ensure output is valid JSON array format.`; logger.warn({ err: error, projectId, attemptCount }, 'PRD extraction attempt failed with error'); } } logger.error({ projectId, maxRetries, lastError: lastError?.message, finalValidationFeedback: validationFeedback }, 'All PRD extraction attempts failed'); return null; } buildEnhancedPRDExtractionPrompt(prdData, taskContext, validationFeedback) { const validAreas = [ 'authentication', 'user-management', 'content-management', 'data-management', 'integration', 'admin', 'ui-components', 'performance', 'frontend', 'backend', 'database' ]; let prompt = `Analyze the following PRD content and extract the most relevant functional areas for this project. PRD Content: ${JSON.stringify(prdData, null, 2)} `; if (taskContext) { prompt += `Current Task Context: Title: ${taskContext.title} Description: ${taskContext.description} Type: ${taskContext.type} `; } prompt += `Valid Functional Areas (you MUST only return areas from this list): ${validAreas.join(', ')} Requirements: 1. Return a JSON array of strings (max 3 functional areas) 2. Areas must be EXACTLY from the valid list above 3. Order by relevance (most important first) 4. Consider the project's domain and features 5. Match areas to PRD features and requirements `; if (validationFeedback) { prompt += `Validation Feedback from Previous Attempt: ${validationFeedback} `; } prompt += `Expected Output Format: ["area1", "area2", "area3"] Example Output: ["authentication", "user-management", "frontend"]`; return prompt; } parseAndValidateLLMFunctionalAreas(llmResult) { const validAreas = [ 'authentication', 'user-management', 'content-management', 'data-management', 'integration', 'admin', 'ui-components', 'performance', 'frontend', 'backend', 'database' ]; try { const parsed = JSON.parse(llmResult.trim()); if (!Array.isArray(parsed)) { logger.debug({ llmResult: llmResult.substring(0, 200) }, 'LLM result is not an array'); return []; } const validatedAreas = parsed .filter((area) => typeof area === 'string') .map((area) => area.toLowerCase().trim()) .filter((area) => validAreas.includes(area)) .slice(0, 3); if (validatedAreas.length === 0) { logger.debug({ parsed, validAreas }, 'No valid functional areas found in LLM response'); } return validatedAreas; } catch (error) { logger.debug({ err: error, llmResult: llmResult.substring(0, 200) }, 'JSON parsing failed, trying regex extraction'); const foundAreas = []; for (const area of validAreas) { if (llmResult.toLowerCase().includes(area)) { foundAreas.push(area); } } return foundAreas.slice(0, 3); } } generateValidationFeedback(llmResult, attemptNumber) { const issues = []; try { JSON.parse(llmResult); } catch { issues.push('Output is not valid JSON'); } if (!llmResult.includes('[') || !llmResult.includes(']')) { issues.push('Output should be a JSON array format'); } const validAreas = [ 'authentication', 'user-management', 'content-management', 'data-management', 'integration', 'admin', 'ui-components', 'performance', 'frontend', 'backend', 'database' ]; const hasValidArea = validAreas.some((area) => llmResult.toLowerCase().includes(area)); if (!hasValidArea) { issues.push('Output should contain valid functional areas from the specified list'); } const feedback = `Attempt ${attemptNumber} issues: ${issues.join('; ')}. Please return a valid JSON array with functional areas from the specified list only.`; return feedback; } extractFromStandardFunctionalAreas(taskContext) { if (!taskContext) { return null; } const text = `${taskContext.title} ${taskContext.description}`.toLowerCase(); const tags = taskContext.tags?.map((tag) => tag.toLowerCase()) || []; const functionalAreaPatterns = { 'authentication': ['auth', 'login', 'register', 'authentication', 'signin', 'signup', 'oauth', 'jwt', 'session', 'security', 'password'], 'user-management': ['user', 'profile', 'account', 'member', 'person', 'customer', 'admin', 'role', 'permission', 'access'], 'content-management': ['content', 'cms', 'article', 'post', 'blog', 'media', 'upload', 'file', 'document', 'asset', 'editor'], 'data-management': ['data', 'crud', 'model', 'entity', 'storage', 'persistence', 'repository', 'query', 'search', 'filter'], 'integration': ['api', 'integration', 'webhook', 'external', 'service', 'third-party', 'connector', 'sync', 'import', 'export'], 'admin': ['admin', 'dashboard', 'management', 'control', 'panel', 'settings', 'configuration', 'monitoring', 'analytics', 'administration'], 'ui-components': ['ui', 'component', 'widget', 'element', 'form', 'button', 'modal', 'layout', 'design', 'interface'], 'performance': ['performance', 'optimization', 'cache', 'speed', 'memory', 'load', 'scalability', 'efficiency', 'benchmark'], 'frontend': ['frontend', 'client', 'browser', 'react', 'vue', 'angular', 'javascript', 'typescript', 'css', 'html'], 'backend': ['backend', 'server', 'api', 'endpoint', 'service', 'controller', 'middleware', 'routing', 'business-logic'], 'database': ['database', 'db', 'sql', 'nosql', 'schema', 'table', 'collection', 'migration', 'index', 'postgres', 'mongo', 'mysql', 'redis'] }; for (const tag of tags) { for (const [area, keywords] of Object.entries(functionalAreaPatterns)) { const functionalArea = area; if (keywords.some((keyword) => tag.includes(keyword))) { return functionalArea; } } } const areaScores = {}; for (const [area, keywords] of Object.entries(functionalAreaPatterns)) { const functionalArea = area; let score = 0; for (const keyword of keywords) { const matches = text.match(new RegExp(keyword, 'g')); score += matches ? matches.length : 0; } if (score > 0) { areaScores[functionalArea] = score; } } const sortedEntries = Object.entries(areaScores); const bestMatch = sortedEntries .sort(([, a], [, b]) => b - a)[0]; return bestMatch ? bestMatch[0] : null; } async extractFunctionalAreaFromTaskContext(taskContext, _config) { logger.warn('extractFunctionalAreaFromTaskContext is deprecated due to circular dependency. Use extractFromStandardFunctionalAreas instead.'); return this.extractFromStandardFunctionalAreas(taskContext); } extractFunctionalAreaSync(taskContext) { return this.extractFunctionalAreaFromKeywords(taskContext); } extractFunctionalAreaFromKeywords(taskContext) { logger.warn('extractFunctionalAreaFromKeywords is deprecated. Use extractFromStandardFunctionalAreas instead.'); return this.extractFromStandardFunctionalAreas(taskContext); } async extractFunctionalAreaFromPRD(projectId, config, taskContext) { try { logger.debug({ projectId }, 'Attempting LLM-powered PRD functional area extraction'); const prdService = PRDIntegrationService.getInstance(); const prdInfo = await prdService.detectExistingPRD(projectId); if (!prdInfo) { logger.debug({ projectId }, 'No PRD found for project'); return []; } const prdResult = await prdService.parsePRD(prdInfo.filePath); if (!prdResult.success || !prdResult.prdData) { logger.warn({ projectId, error: prdResult.error }, 'Failed to parse PRD for functional area extraction'); return []; } const { performFormatAwareLlmCall } = await import('../../../utils/llmHelper.js'); const prdContext = { features: prdResult.prdData.features, technical: prdResult.prdData.technical, overview: prdResult.prdData.overview, taskContext: taskContext ? { title: taskContext.title, description: taskContext.description, type: taskContext.type } : null }; const analysisPrompt = `Analyze the following PRD content and extract the most relevant functional areas for organizing development epics. PRD Context: ${JSON.stringify(prdContext, null, 2)} Please identify 3-5 functional areas that best represent the main development domains in this project. Consider: 1. Features and their primary domains (auth, api, ui, database, etc.) 2. Technical stack and architectural patterns 3. Business goals and user needs 4. Task context if provided Return a JSON array of functional area names (lowercase, single words like "auth", "api", "ui", "database", "media", "admin", etc.). Example response format: ["auth", "api", "ui", "database", "media"]`; const llmResult = await performFormatAwareLlmCall(analysisPrompt, 'You are an expert software architect analyzing PRD content to extract functional areas for development organization.', config, 'prd_integration', 'json'); const functionalAreas = this.parseLLMFunctionalAreas(llmResult); logger.info({ projectId, prdFilePath: prdInfo.filePath, featureCount: prdResult.prdData.features.length, extractedAreas: functionalAreas }, 'Successfully extracted functional areas from PRD using LLM'); return functionalAreas; } catch (error) { logger.warn({ err: error, projectId }, 'LLM-powered PRD functional area extraction failed'); return []; } } parseLLMFunctionalAreas(llmResult) { try { const parsed = JSON.parse(llmResult); if (Array.isArray(parsed)) { const areas = parsed .filter((area) => typeof area === 'string' && area.length > 0) .map((area) => area.toLowerCase().trim()) .slice(0, 5); return areas; } const matches = llmResult.match(/\["?([^"]+)"?(?:,\s*"?([^"]+)"?)*\]/); if (matches) { return matches[0] .replace(/[[\]"]/g, '') .split(',') .map(area => area.trim().toLowerCase()) .filter(area => area.length > 0) .slice(0, 5); } const commonAreas = ['auth', 'api', 'ui', 'database', 'media', 'admin', 'security', 'payment']; const foundAreas = commonAreas.filter(area => llmResult.toLowerCase().includes(area)); return foundAreas.slice(0, 3); } catch (error) { logger.warn({ err: error, llmResult: llmResult.substring(0, 200) }, 'Failed to parse LLM functional areas response'); return []; } } async findExistingEpic(params) { try { const functionalArea = params.functionalArea || await this.extractFunctionalArea(params.taskContext, params.projectId, params.config); if (!functionalArea) { logger.debug({ taskTitle: params.taskContext?.title }, 'No functional area extracted, skipping existing epic search'); return null; } const projectOps = getProjectOperations(); const projectResult = await projectOps.getProject(params.projectId); if (!projectResult.success || !projectResult.data) { return null; } const project = projectResult.data; if (!project.epicIds || project.epicIds.length === 0) { logger.debug({ functionalArea }, 'No epics exist in project yet'); return null; } logger.debug({ functionalArea, projectEpicIds: project.epicIds, taskTitle: params.taskContext?.title }, 'Searching for existing epic with exact functional area match'); const storageManager = await getStorageManager(); for (const epicId of project.epicIds) { const epicResult = await storageManager.getEpic(epicId); if (epicResult.success && epicResult.data) { const epic = epicResult.data; logger.debug({ epicId: epic.id, epicTitle: epic.title, epicTags: epic.metadata.tags, searchingFor: functionalArea }, 'Checking epic for exact functional area match'); if (epic.metadata.tags && epic.metadata.tags.includes(functionalArea)) { logger.debug({ epicId: epic.id, functionalArea }, 'Found exact functional area match'); return { epicId: epic.id, epicName: epic.title, source: 'existing', confidence: 0.9, created: false }; } } } logger.debug({ functionalArea }, 'No exact functional area match found, will create new epic'); return null; } catch (error) { logger.debug({ err: error, projectId: params.projectId }, 'Failed to find existing epic'); return null; } } async generateEnhancedEpicContext(functionalArea, params) { try { if (!params.config) { logger.debug({ functionalArea }, 'No config provided for LLM epic enhancement'); return null; } let prdContext = null; if (params.projectId) { try { const prdService = PRDIntegrationService.getInstance(); const prdInfo = await prdService.detectExistingPRD(params.projectId); if (prdInfo) { const prdResult = await prdService.parsePRD(prdInfo.filePath); if (prdResult.success && prdResult.prdData) { prdContext = { features: prdResult.prdData.features.filter(f => f.title.toLowerCase().includes(functionalArea) || f.description.toLowerCase().includes(functionalArea)), businessGoals: prdResult.prdData.overview.businessGoals, productGoals: prdResult.prdData.overview.productGoals, technical: prdResult.prdData.technical }; } } } catch (error) { logger.debug({ err: error }, 'Could not fetch PRD context for epic enhancement'); } } const { performFormatAwareLlmCall } = await import('../../../utils/llmHelper.js'); const epicGenerationPrompt = `Generate an enhanced epic title and description for a development epic focused on the "${functionalArea}" functional area. Context: - Functional Area: ${functionalArea} - Project ID: ${params.projectId || 'unknown'} ${params.taskContext ? `- Task Context: ${JSON.stringify(params.taskContext, null, 2)}` : ''} ${prdContext ? `- PRD Context: ${JSON.stringify(prdContext, null, 2)}` : '- No PRD context available'} Please generate: 1. A concise, descriptive epic title that reflects the functional area and business context 2. A comprehensive epic description that includes business value and scope 3. A suggested priority level (low, medium, high, critical) 4. Relevant tags for categorization Return a JSON object with this structure: { "title": "Epic Title Here", "description": "Detailed epic description that explains the business value, scope, and context...", "priority": "medium", "tags": ["functionalArea", "additional", "tags"] } Guidelines: - Title should be 3-8 words, business-focused - Description should explain WHY this epic matters, not just WHAT it includes - Priority should reflect business impact and dependencies - Tags should include the functional area plus relevant categorization`; const llmResult = await performFormatAwareLlmCall(epicGenerationPrompt, 'You are an expert product manager and software architect creating meaningful development epics with business context.', params.config, 'epic_generation', 'json'); const epicContext = this.parseEpicGenerationResult(llmResult); if (epicContext) { logger.info({ functionalArea, projectId: params.projectId, generatedTitle: epicContext.title, hasPrdContext: !!prdContext }, 'Generated enhanced epic context using LLM'); return epicContext; } return null; } catch (error) { logger.warn({ err: error, functionalArea, projectId: params.projectId }, 'Failed to generate enhanced epic context, using fallback'); return null; } } parseEpicGenerationResult(llmResult) { try { const parsed = JSON.parse(llmResult); if (parsed && typeof parsed === 'object' && typeof parsed.title === 'string' && typeof parsed.description === 'string') { let validTags; if (Array.isArray(parsed.tags)) { const stringTags = parsed.tags.filter((tag) => typeof tag === 'string' && tag.length > 0); validTags = stringTags.length > 0 ? stringTags : undefined; } return { title: parsed.title.trim(), description: parsed.description.trim(), priority: this.validatePriority(parsed.priority), tags: validTags }; } const titleMatch = llmResult.match(/"title"\s*:\s*"([^"]+)"/); const descMatch = llmResult.match(/"description"\s*:\s*"([^"]+)"/); if (titleMatch && descMatch) { return { title: titleMatch[1].trim(), description: descMatch[1].trim() }; } return null; } catch (error) { logger.debug({ err: error, llmResult: typeof llmResult === 'string' ? llmResult.substring(0, 200) : 'non-string result' }, 'Failed to parse epic generation result'); return null; } } validatePriority(priority) { if (typeof priority === 'string') { const normalizedPriority = priority.toLowerCase(); const validPriorities = ['low', 'medium', 'high', 'critical']; if (validPriorities.includes(normalizedPriority)) { return normalizedPriority; } } return undefined; } async createFunctionalAreaEpic(params) { try { const functionalArea = params.functionalArea || await this.extractFunctionalArea(params.taskContext, params.projectId, params.config); if (!functionalArea) { return null; } const epicService = getEpicService(); const { getVibeTaskManagerConfig } = await import('../utils/config-loader.js'); const config = await getVibeTaskManagerConfig(); const epicTimeLimit = config?.taskManager?.rddConfig?.epicTimeLimit || 400; const enhancedContext = await this.generateEnhancedEpicContext(functionalArea, params); const epicTitle = enhancedContext?.title || `${functionalArea.charAt(0).toUpperCase() + functionalArea.slice(1)} Epic`; const epicDescription = enhancedContext?.description || `Epic for ${functionalArea} related tasks and features`; const createParams = { title: epicTitle, description: epicDescription, projectId: params.projectId, priority: enhancedContext?.priority || params.priority || 'medium', estimatedHours: params.estimatedHours || epicTimeLimit, tags: enhancedContext?.tags ? [...enhancedContext.tags] : [functionalArea, 'auto-created'] }; logger.info({ functionalArea, epicTitle, projectId: params.projectId, createParams }, 'Attempting to create functional area epic'); const createResult = await epicService.createEpic(createParams, 'epic-context-resolver'); logger.info({ createResult: { success: createResult.success, error: createResult.error, dataExists: !!createResult.data, epicId: createResult.data?.id }, functionalArea, projectId: params.projectId }, 'Epic creation result'); if (createResult.success && createResult.data) { await this.updateProjectEpicAssociation(params.projectId, createResult.data.id); logger.info({ epicId: createResult.data.id, epicTitle, functionalArea, projectId: params.projectId, source: 'created' }, 'Successfully created functional area epic'); return { epicId: createResult.data.id, epicName: epicTitle, source: 'created', confidence: 0.8, created: true }; } logger.warn({ functionalArea, projectId: params.projectId, createResultSuccess: createResult.success, createResultError: createResult.error, hasData: !!createResult.data }, 'Epic creation failed - no epic data returned'); return null; } catch (error) { logger.debug({ err: error, projectId: params.projectId }, 'Failed to create functional area epic'); return null; } } async createMainEpic(params) { try { const epicService = getEpicService(); const epicTitle = 'Main Epic'; const epicDescription = 'Main epic for project tasks and features'; const createResult = await epicService.createEpic({ title: epicTitle, description: epicDescription, projectId: params.projectId, priority: params.priority || 'medium', estimatedHours: params.estimatedHours || 80, tags: ['main', 'auto-created'] }, 'epic-context-resolver'); if (createResult.success && createResult.data) { await this.updateProjectEpicAssociation(params.projectId, createResult.data.id); return { epicId: createResult.data.id, epicName: epicTitle, source: 'created', confidence: 0.6, created: true }; } return { epicId: `${params.projectId}-main-epic`, epicName: 'Main Epic', source: 'fallback', confidence: 0.3, created: false }; } catch (error) { logger.warn({ err: error, projectId: params.projectId }, 'Failed to create main epic, using fallback'); return { epicId: `${params.projectId}-main-epic`, epicName: 'Main Epic', source: 'fallback', confidence: 0.1, created: false }; } } async addTaskToEpic(taskId, epicId, _projectId) { try { const storageManager = await getStorageManager(); const [taskResult, epicResult] = await Promise.all([ storageManager.getTask(taskId), storageManager.getEpic(epicId) ]); if (!taskResult.success || !taskResult.data || !epicResult.success || !epicResult.data) { throw new Error('Task or epic not found'); } const task = taskResult.data; const epic = epicResult.data; task.epicId = epicId; task.metadata.updatedAt = new Date(); if (!epic.taskIds.includes(taskId)) { epic.taskIds.push(taskId); epic.metadata.updatedAt = new Date(); } const [taskUpdateResult, epicUpdateResult] = await Promise.all([ storageManager.updateTask(taskId, task), storageManager.updateEpic(epicId, epic) ]); if (!taskUpdateResult.success || !epicUpdateResult.success) { throw new Error('Failed to update task-epic relationship'); } const progressData = await this.calculateEpicProgress(epicId); logger.debug({ taskId, epicId, progress: progressData.progressPercentage }, 'Added task to epic'); return { success: true, epicId, taskId, relationshipType: 'added', metadata: { epicProgress: progressData.progressPercentage, taskCount: progressData.totalTasks, completedTaskCount: progressData.completedTasks, conflictsResolved: await this.resolveResourceConflicts(epicId) } }; } catch (error) { logger.error({ err: error, taskId, epicId }, 'Failed to add task to epic'); return { success: false, epicId, taskId, relationshipType: 'added', metadata: {} }; } } async moveTaskBetweenEpics(taskId, fromEpicId, toEpicId, _projectId) { try { const storageManager = await getStorageManager(); const [taskResult, fromEpicResult, toEpicResult] = await Promise.all([ storageManager.getTask(taskId), storageManager.getEpic(fromEpicId), storageManager.getEpic(toEpicId) ]); if (!taskResult.success || !taskResult.data) { throw new Error('Task not found'); } const task = taskResult.data; if (fromEpicResult.success && fromEpicResult.data) { const fromEpic = fromEpicResult.data; fromEpic.taskIds = fromEpic.taskIds.filter((id) => id !== taskId); fromEpic.metadata.updatedAt = new Date(); await storageManager.updateEpic(fromEpicId, fromEpic); } if (toEpicResult.success && toEpicResult.data) { const toEpic = toEpicResult.data; if (!toEpic.taskIds.includes(taskId)) { toEpic.taskIds.push(taskId); toEpic.metadata.updatedAt = new Date(); await storageManager.updateEpic(toEpicId, toEpic); } } task.epicId = toEpicId; task.metadata.updatedAt = new Date(); await storageManager.updateTask(taskId, task); const [fromProgress, toProgress] = await Promise.all([ this.calculateEpicProgress(fromEpicId), this.calculateEpicProgress(toEpicId) ]); const conflictsResolved = await this.resolveResourceConflicts(toEpicId); logger.info({ taskId, fromEpicId, toEpicId, fromProgress: fromProgress.progressPercentage, toProgress: toProgress.progressPercentage, conflictsResolved }, 'Moved task between epics'); return { success: true, epicId: toEpicId, taskId, relationshipType: 'moved', previousEpicId: fromEpicId, metadata: { epicProgress: toProgress.progressPercentage, taskCount: toProgress.totalTasks, completedTaskCount: toProgress.completedTasks, conflictsResolved } }; } catch (error) { logger.error({ err: error, taskId, fromEpicId, toEpicId }, 'Failed to move task between epics'); return { success: false, epicId: toEpicId, taskId, relationshipType: 'moved', previousEpicId: fromEpicId, metadata: {} }; } } async calculateEpicProgress(epicId) { try { const storageManager = await getStorageManager(); const epicResult = await storageManager.getEpic(epicId); if (!epicResult || !epicResult.success || !epicResult.data) { throw new Error('Epic not found'); } const epic = epicResult.data; const taskPromises = epic.taskIds.map((taskId) => storageManager.getTask(taskId)); const taskResults = await Promise.all(taskPromises); const tasks = taskResults .filter(result => result && result.success && result.data) .map(result => result.data); const totalTasks = tasks.length; const completedTasks = tasks.filter(task => task.status === 'completed').length; const inProgressTasks = tasks.filter(task => task.status === 'in_progress').length; const blockedTasks = tasks.filter(task => task.status === 'blocked').length; const progressPercentage = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0; const filePathConflicts = this.detectFilePathConflicts(tasks); const dependencyComplexity = await this.calculateDependencyComplexity(epic.taskIds); const parallelizableTaskGroups = await this.identifyParallelizableGroups(epic.taskIds); const estimatedCompletionDate = this.estimateCompletionDate(tasks, progressPercentage); const progressData = { epicId, totalTasks, completedTasks, inProgressTasks, blockedTasks, progressPercentage, estimatedCompletionDate, resourceUtilization: { filePathConflicts, dependencyComplexity, parallelizableTaskGroups } }; logger.debug({ epicId, progressData }, 'Calculated epic progress'); return progressData; } catch (error) { logger.error({ err: error, epicId }, 'Failed to calculate epic progress'); return { epicId, totalTasks: 0, completedTasks: 0, inProgressTasks: 0, blockedTasks: 0, progressPercentage: 0, resourceUtilization: { filePathConflicts: 0, dependencyComplexity: 0, parallelizableTaskGroups: 0 } }; } } async updateEpicStatusFromTasks(epicId) { try { const progressData = await this.calculateEpicProgress(epicId); const storageManager = await getStorageManager(); const epicResult = await storageManager.getEpic(epicId); if (!epicResult.success || !epicResult.data) { return false; } const epic = epicResult.data; let statusChanged = false; let newStatus = epic.status; if (progressData.totalTasks === 0) { newStatus = 'pending'; } else if (progressData.completedTasks === progressData.totalTasks) { newStatus = 'completed'; } else if (progressData.inProgressTasks > 0 || progressData.completedTasks > 0) { newStatus = 'in_progress'; } else if (progressData.blockedTasks === progressData.totalTasks) { newStatus = 'blocked'; } else { newStatus = 'pending'; } if (newStatus !== epic.status) { epic.status = newStatus; epic.metadata.updatedAt = new Date(); const updateResult = await storageManager.updateEpic(epicId, epic); if (updateResult.success) { statusChanged = true; logger.info({ epicId, oldStatus: epic.status, newStatus, progressData }, 'Updated epic status from task completion'); } } return statusChanged; } catch (error) { logger.error({ err: error, epicId }, 'Failed to update epic status from tasks'); return false; } } async resolveResourceConflicts(epicId) { try { const storageManager = await getStorageManager(); const epicResult = await storageManager.getEpic(epicId); if (!epicResult || !epicResult.success || !epicResult.data) { return 0; } const epic = epicResult.data; const taskPromises = epic.taskIds.map((taskId) => storageManager.getTask(taskId)); const taskResults = await Promise.all(taskPromises); const tasks = taskResults .filter(result => result && result.success && result.data) .map(result => result.data); const conflicts = this.detectFilePathConflicts(tasks); if (conflicts > 0) { logger.warn({ epicId, conflicts }, 'Detected file path conflicts in epic tasks'); } return conflicts; } catch (error) { logger.error({ err: error, epicId }, 'Failed to resolve resource conflicts'); return 0; } } detectFilePathConflicts(tasks) { const filePathMap = new Map(); tasks.forEach(task => { const atomicTask = task; if (atomicTask.filePaths && Array.isArray(atomicTask.filePaths)) { atomicTask.filePaths.forEach((filePath) => { if (!filePathMap.has(filePath)) { filePathMap.set(filePath, []); } filePathMap.get(filePath).push(atomicTask.id); }); } }); let conflicts = 0; filePathMap.forEach((taskIds) => { if (taskIds.length > 1) { conflicts++; } }); return conflicts; } async calculateDependencyComplexity(taskIds) { try { const storageManager = await getStorageManager(); let totalDependencies = 0; for (const taskId of taskIds) { const dependencies = await storageManager.getDependenciesForTask(taskId); if (dependencies.success && dependencies.data) { totalDependencies += dependencies.data.length; } } const complexity = Math.min(Math.floor(totalDependencies / taskIds.length), 10); return complexity; } catch (error) { logger.debug({ err: error, taskIds }, 'Failed to calculate dependency complexity'); return 0; } } async identifyParallelizableGroups(taskIds) { try { const storageManager = await getStorageManager(); let parallelizable = 0; for (const taskId of taskIds) { const dependencies = await storageManager.getDependenciesForTask(taskId); if (dependencies.success && dependencies.data && dependencies.data.length === 0) { parallelizable++; } } return parallelizable; } catch (error) { logger.debug({ err: error, taskIds }, 'Failed to identify parallelizable groups'); return 0; } } estimateCompletionDate(tasks, progressPercentage) { if (tasks.length === 0 || progressPercentage >= 100) { return undefined; } const totalEstimatedHours = tasks.reduce((sum, task) => sum + (task.estimatedHours || 0), 0); const remainingHours = totalEstimatedHours * ((100 - progressPercentage) / 100); const workingDaysRemaining = Math.ceil(remainingHours / 8); const estimatedDate = new Date(); estimatedDate.setDate(estimatedDate.getDate() + workingDaysRemaining); return estimatedDate; } async updateProjectEpicAssociation(projectId, epicId) { try { const storageManager = await getStorageManager(); const projectResult = await storageManager.getProject(projectId); if (projectResult.success && projectResult.data) { const project = projectResult.data; if (!project.epicIds.includes(epicId)) { project.epicIds.push(epicId); project.metadata.updatedAt = new Date(); const updateResult = await storageManager.updateProject(projectId, project); if (updateResult.success) { logger.debug({ projectId, epicId }, 'Updated project epic association'); } else { logger.warn({ projectId, epicId, error: updateResult.error }, 'Failed to update project epic association'); } } } } catch (error) { logger.warn({ err: error, projectId, epicId }, 'Failed to update project epic association'); } } async createProjectSpecificFallbackEpic(params) { try { const epicService = getEpicService(); const { getVibeTaskManagerConfig } = await import('../utils/config-loader.js'); const config = await getVibeTaskManagerConfig(); const epicTimeLimit = config?.taskManager?.rddConfig?.epicTimeLimit || 400; let projectName = 'Unknown Project'; let projectDescription = 'Project tasks and features'; try { const storageManager = await getStorageManager(); const projectResult = await storageManager.getProject(params.projectId); if (projectResult.success && projectResult.data) { projectName = projectResult.data.name; projectDescription = projectResult.data.description || projectDescription; } } catch (contextError) { logger.debug({ err: contextError }, 'Could not fetch project context for epic naming'); } let epicTitle = `${projectName} Development Epic`; let epicDescription = `Main development epic for ${projectName}: ${projectDescription}`; if (params.taskContext) { const taskType = params.taskContext.type; const taskTitle = params.taskContext.title; if (taskType === 'development') { epicTitle = `${projectName} Development Tasks`; epicDescription = `Development epic for ${projectName} including: ${taskTitle}`; } else if (taskType === 'testing') { epicTitle = `${projectName} Testing & QA`; epicDescription = `Testing and quality assurance epic for ${projectName}`; } else if (taskType === 'documentation') { epicTitle = `${projectName} Documentation`; epicDescription = `Documentation epic for ${projectName}`; } else { epicTitle = `${projectName} ${taskType.charAt(0).toUpperCase() + taskType.slice(1)} Epic`; epicDescription = `${taskType} epic for ${projectName}: ${taskTitle}`; } } const createResult = await epicService.createEpic({ title: epicTitle, description: epicDescription, projectId: params.projectId, priority: params.priority || 'medium', estimatedHours: params.estimatedHours || epicTimeLimit, tags: ['auto-created', 'fallback', 'project-specific'] }, 'epic-context-resolver-fallback'); if (createResult.success && createResult.data) { await this.updateProjectEpicAssociation(params.projectId, createResult.data.id); logger.info({ projectId: params.projectId, epicId: createResult.data.id, epicTitle, source: 'fallback' }, 'Created project-specific fallback epic'); return { epicId: createResult.data.id, epicName: epicTitle, source: 'created', confidence: 0.6, created: true }; } throw new Error(`Fai