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.

453 lines (452 loc) 21.8 kB
import { getVibeTaskManagerOutputDir } from '../utils/config-loader.js'; import { FileUtils } from '../utils/file-utils.js'; import logger from '../../../logger.js'; import * as path from 'path'; import * as fs from 'fs-extra'; const DEFAULT_SUMMARY_CONFIG = { includeTaskBreakdown: true, includeDependencyAnalysis: true, includePerformanceMetrics: true, includeVisualDiagrams: true, includeJsonExports: true }; export class DecompositionSummaryGenerator { config; constructor(config = {}) { this.config = { ...DEFAULT_SUMMARY_CONFIG, ...config }; } async generateSessionSummary(session) { const startTime = Date.now(); try { logger.info({ sessionId: session.id, projectId: session.projectId, status: session.status }, 'Starting decomposition summary generation'); const outputDirectory = await this.createSessionDirectory(session); const generatedFiles = []; const taskAnalysis = this.analyzeSessionTasks(session); if (this.config.includeTaskBreakdown) { const summaryFile = await this.generateMainSummary(session, taskAnalysis, outputDirectory); generatedFiles.push(summaryFile); } const taskBreakdownFile = await this.generateTaskBreakdown(session, outputDirectory); generatedFiles.push(taskBreakdownFile); if (this.config.includePerformanceMetrics) { const metricsFile = await this.generatePerformanceMetrics(session, outputDirectory); generatedFiles.push(metricsFile); } if (this.config.includeDependencyAnalysis && session.persistedTasks) { const dependencyFile = await this.generateDependencyAnalysis(session, outputDirectory); generatedFiles.push(dependencyFile); } if (this.config.includeVisualDiagrams) { const diagramFiles = await this.generateVisualDiagrams(session, outputDirectory); generatedFiles.push(...diagramFiles); } if (this.config.includeJsonExports) { const jsonFiles = await this.generateJsonExports(session, taskAnalysis, outputDirectory); generatedFiles.push(...jsonFiles); } const generationTime = Date.now() - startTime; logger.info({ sessionId: session.id, projectId: session.projectId, outputDirectory, filesGenerated: generatedFiles.length, generationTime }, 'Decomposition summary generation completed successfully'); return { success: true, outputDirectory, generatedFiles, metadata: { sessionId: session.id, projectId: session.projectId, totalTasks: session.persistedTasks?.length || 0, totalHours: taskAnalysis.totalHours, generationTime, timestamp: new Date() } }; } catch (error) { const generationTime = Date.now() - startTime; logger.error({ err: error, sessionId: session.id, projectId: session.projectId, generationTime }, 'Failed to generate decomposition summary'); return { success: false, outputDirectory: '', generatedFiles: [], error: error instanceof Error ? error.message : String(error), metadata: { sessionId: session.id, projectId: session.projectId, totalTasks: 0, totalHours: 0, generationTime, timestamp: new Date() } }; } } async createSessionDirectory(session) { const baseOutputDir = this.config.customOutputDir || getVibeTaskManagerOutputDir(); const sessionDir = path.join(baseOutputDir, 'decomposition-sessions', `${session.projectId}-${session.id}`); await fs.ensureDir(sessionDir); return sessionDir; } analyzeSessionTasks(session) { const tasks = session.persistedTasks || []; if (tasks.length === 0) { return { totalTasks: 0, totalHours: 0, averageHours: 0, tasksByType: {}, tasksByPriority: {}, complexityDistribution: { simple: 0, medium: 0, complex: 0 }, estimatedDuration: { minimum: 0, maximum: 0, average: 0 } }; } const totalHours = tasks.reduce((sum, task) => sum + (task.estimatedHours || 0), 0); const averageHours = totalHours / tasks.length; const tasksByType = {}; tasks.forEach(task => { tasksByType[task.type] = (tasksByType[task.type] || 0) + 1; }); const tasksByPriority = {}; tasks.forEach(task => { tasksByPriority[task.priority] = (tasksByPriority[task.priority] || 0) + 1; }); const complexityDistribution = { simple: tasks.filter(t => (t.estimatedHours || 0) <= 2).length, medium: tasks.filter(t => (t.estimatedHours || 0) > 2 && (t.estimatedHours || 0) <= 8).length, complex: tasks.filter(t => (t.estimatedHours || 0) > 8).length }; const hours = tasks.map(t => t.estimatedHours || 0); const estimatedDuration = { minimum: Math.min(...hours), maximum: Math.max(...hours), average: averageHours }; return { totalTasks: tasks.length, totalHours, averageHours, tasksByType, tasksByPriority, complexityDistribution, estimatedDuration }; } async generateMainSummary(session, analysis, outputDir) { const timestamp = new Date().toISOString(); const duration = session.endTime ? session.endTime.getTime() - session.startTime.getTime() : Date.now() - session.startTime.getTime(); let content = `# Decomposition Session Summary\n\n`; content += `**Session ID:** ${session.id}\n`; content += `**Project ID:** ${session.projectId}\n`; content += `**Status:** ${session.status}\n`; content += `**Generated:** ${timestamp}\n\n`; content += `## Session Overview\n\n`; content += `- **Start Time:** ${session.startTime.toISOString()}\n`; content += `- **End Time:** ${session.endTime?.toISOString() || 'In Progress'}\n`; content += `- **Duration:** ${Math.round(duration / 1000)}s\n`; content += `- **Progress:** ${session.progress}%\n`; content += `- **Max Depth:** ${session.maxDepth}\n`; content += `- **Current Depth:** ${session.currentDepth}\n\n`; content += `## Task Analysis\n\n`; content += `- **Total Tasks Generated:** ${analysis.totalTasks}\n`; content += `- **Total Estimated Hours:** ${analysis.totalHours.toFixed(1)}h\n`; content += `- **Average Hours per Task:** ${analysis.averageHours.toFixed(1)}h\n\n`; content += `### Task Distribution by Type\n\n`; Object.entries(analysis.tasksByType).forEach(([type, count]) => { content += `- **${type}:** ${count} tasks\n`; }); content += `\n### Task Distribution by Priority\n\n`; Object.entries(analysis.tasksByPriority).forEach(([priority, count]) => { content += `- **${priority}:** ${count} tasks\n`; }); content += `\n### Complexity Distribution\n\n`; content += `- **Simple (≤2h):** ${analysis.complexityDistribution.simple} tasks\n`; content += `- **Medium (2-8h):** ${analysis.complexityDistribution.medium} tasks\n`; content += `- **Complex (>8h):** ${analysis.complexityDistribution.complex} tasks\n\n`; if (session.error) { content += `## Error Information\n\n`; content += `**Error:** ${session.error}\n\n`; } content += `---\n`; content += `*Generated by Vibe Task Manager Decomposition Summary Generator*\n`; const filePath = path.join(outputDir, 'session-summary.md'); await FileUtils.writeFile(filePath, content); return filePath; } async generateTaskBreakdown(session, outputDir) { const tasks = session.persistedTasks || []; let content = `# Detailed Task Breakdown\n\n`; content += `**Session:** ${session.id}\n`; content += `**Project:** ${session.projectId}\n`; content += `**Total Tasks:** ${tasks.length}\n\n`; if (tasks.length === 0) { content += `No tasks were generated in this session.\n`; } else { tasks.forEach((task, index) => { content += `## Task ${index + 1}: ${task.title}\n\n`; content += `- **ID:** ${task.id}\n`; content += `- **Type:** ${task.type}\n`; content += `- **Priority:** ${task.priority}\n`; content += `- **Status:** ${task.status}\n`; content += `- **Estimated Hours:** ${task.estimatedHours || 0}h\n`; content += `- **Epic ID:** ${task.epicId || 'N/A'}\n\n`; content += `**Description:**\n${task.description}\n\n`; if (task.acceptanceCriteria.length > 0) { content += `**Acceptance Criteria:**\n`; task.acceptanceCriteria.forEach((criteria, i) => { content += `${i + 1}. ${criteria}\n`; }); content += `\n`; } if (task.filePaths.length > 0) { content += `**File Paths:**\n`; task.filePaths.forEach(filePath => { content += `- ${filePath}\n`; }); content += `\n`; } if (task.dependencies.length > 0) { content += `**Dependencies:**\n`; task.dependencies.forEach(dep => { content += `- ${dep}\n`; }); content += `\n`; } if (task.tags.length > 0) { content += `**Tags:** ${task.tags.join(', ')}\n\n`; } content += `---\n\n`; }); } const filePath = path.join(outputDir, 'task-breakdown.md'); await FileUtils.writeFile(filePath, content); return filePath; } async generatePerformanceMetrics(session, outputDir) { const duration = session.endTime ? session.endTime.getTime() - session.startTime.getTime() : Date.now() - session.startTime.getTime(); const tasks = session.persistedTasks || []; const totalHours = tasks.reduce((sum, task) => sum + (task.estimatedHours || 0), 0); let content = `# Performance Metrics\n\n`; content += `**Session:** ${session.id}\n`; content += `**Project:** ${session.projectId}\n\n`; content += `## Timing Metrics\n\n`; content += `- **Total Duration:** ${Math.round(duration / 1000)}s (${(duration / 60000).toFixed(2)} minutes)\n`; content += `- **Start Time:** ${session.startTime.toISOString()}\n`; content += `- **End Time:** ${session.endTime?.toISOString() || 'In Progress'}\n`; content += `- **Progress:** ${session.progress}%\n\n`; content += `## Decomposition Metrics\n\n`; content += `- **Max Depth:** ${session.maxDepth}\n`; content += `- **Current Depth:** ${session.currentDepth}\n`; content += `- **Total Tasks Processed:** ${session.processedTasks}\n`; content += `- **Tasks Generated:** ${tasks.length}\n`; content += `- **Total Estimated Work:** ${totalHours.toFixed(1)} hours\n\n`; content += `## Efficiency Metrics\n\n`; if (duration > 0) { const tasksPerSecond = tasks.length / (duration / 1000); const hoursPerSecond = totalHours / (duration / 1000); content += `- **Tasks Generated per Second:** ${tasksPerSecond.toFixed(3)}\n`; content += `- **Work Hours Planned per Second:** ${hoursPerSecond.toFixed(3)}\n`; content += `- **Average Task Generation Time:** ${(duration / tasks.length / 1000).toFixed(2)}s per task\n\n`; } if (session.results.length > 0) { content += `## Decomposition Results\n\n`; session.results.forEach((result, index) => { content += `### Result ${index + 1}\n`; content += `- **Success:** ${result.success}\n`; content += `- **Is Atomic:** ${result.isAtomic}\n`; content += `- **Depth:** ${result.depth}\n`; content += `- **Sub-tasks:** ${result.subTasks.length}\n`; if (result.error) { content += `- **Error:** ${result.error}\n`; } content += `\n`; }); } const filePath = path.join(outputDir, 'performance-metrics.md'); await FileUtils.writeFile(filePath, content); return filePath; } async generateDependencyAnalysis(session, outputDir) { const tasks = session.persistedTasks || []; let content = `# Dependency Analysis\n\n`; content += `**Session:** ${session.id}\n`; content += `**Project:** ${session.projectId}\n\n`; const dependencyMap = new Map(); const dependentMap = new Map(); tasks.forEach(task => { dependencyMap.set(task.id, task.dependencies); task.dependencies.forEach(depId => { if (!dependentMap.has(depId)) { dependentMap.set(depId, []); } dependentMap.get(depId).push(task.id); }); }); const totalDependencies = Array.from(dependencyMap.values()).flat().length; const tasksWithDependencies = Array.from(dependencyMap.values()).filter(deps => deps.length > 0).length; const orphanedTasks = tasks.filter(task => task.dependencies.length === 0 && (!dependentMap.has(task.id) || dependentMap.get(task.id).length === 0)); content += `## Overview\n\n`; content += `- **Total Tasks:** ${tasks.length}\n`; content += `- **Total Dependencies:** ${totalDependencies}\n`; content += `- **Tasks with Dependencies:** ${tasksWithDependencies}\n`; content += `- **Orphaned Tasks:** ${orphanedTasks.length}\n\n`; if (orphanedTasks.length > 0) { content += `## Orphaned Tasks (No Dependencies)\n\n`; orphanedTasks.forEach(task => { content += `- **${task.title}** (${task.id})\n`; }); content += `\n`; } content += `## Task Dependencies\n\n`; tasks.forEach(task => { if (task.dependencies.length > 0) { content += `### ${task.title} (${task.id})\n`; content += `**Depends on:**\n`; task.dependencies.forEach(depId => { const depTask = tasks.find(t => t.id === depId); content += `- ${depTask?.title || depId} (${depId})\n`; }); content += `\n`; } }); const filePath = path.join(outputDir, 'dependency-analysis.md'); await FileUtils.writeFile(filePath, content); return filePath; } async generateVisualDiagrams(session, outputDir) { const tasks = session.persistedTasks || []; const files = []; const taskFlowDiagram = this.generateTaskFlowDiagram(tasks, session); const taskFlowFile = path.join(outputDir, 'task-flow-diagram.md'); await FileUtils.writeFile(taskFlowFile, taskFlowDiagram); files.push(taskFlowFile); if (tasks.some(task => task.dependencies.length > 0)) { const dependencyDiagram = this.generateDependencyDiagram(tasks, session); const dependencyFile = path.join(outputDir, 'dependency-diagram.md'); await FileUtils.writeFile(dependencyFile, dependencyDiagram); files.push(dependencyFile); } return files; } async generateJsonExports(session, analysis, outputDir) { const files = []; const sessionData = { session: { id: session.id, projectId: session.projectId, status: session.status, startTime: session.startTime, endTime: session.endTime, progress: session.progress, maxDepth: session.maxDepth, currentDepth: session.currentDepth, totalTasks: session.totalTasks, processedTasks: session.processedTasks, error: session.error }, analysis, tasks: session.persistedTasks || [], results: session.results, richResults: session.richResults }; const sessionFile = path.join(outputDir, 'session-data.json'); await FileUtils.writeFile(sessionFile, JSON.stringify(sessionData, null, 2)); files.push(sessionFile); if (session.persistedTasks && session.persistedTasks.length > 0) { const tasksFile = path.join(outputDir, 'tasks.json'); await FileUtils.writeFile(tasksFile, JSON.stringify(session.persistedTasks, null, 2)); files.push(tasksFile); } const analysisFile = path.join(outputDir, 'analysis-summary.json'); await FileUtils.writeFile(analysisFile, JSON.stringify(analysis, null, 2)); files.push(analysisFile); return files; } generateTaskFlowDiagram(tasks, session) { let content = `# Task Flow Diagram\n\n`; content += `**Session:** ${session.id}\n`; content += `**Project:** ${session.projectId}\n\n`; content += `\`\`\`mermaid\n`; content += `graph TD\n`; content += ` Start([Decomposition Started])\n`; if (tasks.length === 0) { content += ` Start --> NoTasks[No Tasks Generated]\n`; } else { const tasksByType = tasks.reduce((acc, task) => { if (!acc[task.type]) acc[task.type] = []; acc[task.type].push(task); return acc; }, {}); content += ` Start --> Decomp[Task Decomposition]\n`; Object.entries(tasksByType).forEach(([type, typeTasks]) => { const typeNode = `Type_${type.replace(/[^a-zA-Z0-9]/g, '_')}`; content += ` Decomp --> ${typeNode}[${type} Tasks: ${typeTasks.length}]\n`; typeTasks.slice(0, 5).forEach((task) => { const taskNode = `Task_${task.id.replace(/[^a-zA-Z0-9]/g, '_')}`; const taskTitle = task.title.length > 30 ? task.title.substring(0, 30) + '...' : task.title; content += ` ${typeNode} --> ${taskNode}["${taskTitle}<br/>${task.estimatedHours}h"]\n`; }); if (typeTasks.length > 5) { content += ` ${typeNode} --> More_${typeNode}[... ${typeTasks.length - 5} more tasks]\n`; } }); content += ` Decomp --> Complete([Decomposition Complete])\n`; } content += `\`\`\`\n\n`; content += `## Legend\n\n`; content += `- **Rectangles**: Task groups by type\n`; content += `- **Rounded rectangles**: Individual tasks with estimated hours\n`; content += `- **Circles**: Process start/end points\n`; return content; } generateDependencyDiagram(tasks, session) { let content = `# Dependency Diagram\n\n`; content += `**Session:** ${session.id}\n`; content += `**Project:** ${session.projectId}\n\n`; content += `\`\`\`mermaid\n`; content += `graph LR\n`; tasks.forEach(task => { const nodeId = `T_${task.id.replace(/[^a-zA-Z0-9]/g, '_')}`; const taskTitle = task.title.length > 20 ? task.title.substring(0, 20) + '...' : task.title; content += ` ${nodeId}["${taskTitle}<br/>${task.estimatedHours}h"]:::${task.priority}\n`; }); tasks.forEach(task => { if (task.dependencies.length > 0) { const taskNodeId = `T_${task.id.replace(/[^a-zA-Z0-9]/g, '_')}`; task.dependencies.forEach(depId => { const depNodeId = `T_${depId.replace(/[^a-zA-Z0-9]/g, '_')}`; content += ` ${depNodeId} --> ${taskNodeId}\n`; }); } }); content += ` classDef high fill:#ffcccc,stroke:#ff0000,stroke-width:2px\n`; content += ` classDef medium fill:#ffffcc,stroke:#ffaa00,stroke-width:2px\n`; content += ` classDef low fill:#ccffcc,stroke:#00aa00,stroke-width:2px\n`; content += `\`\`\`\n\n`; content += `## Legend\n\n`; content += `- **Red**: High priority tasks\n`; content += `- **Yellow**: Medium priority tasks\n`; content += `- **Green**: Low priority tasks\n`; content += `- **Arrows**: Dependency relationships (from dependency to dependent)\n`; return content; } }