UNPKG

claude-flow-tbowman01

Version:

Enterprise-grade AI agent orchestration with ruv-swarm integration (Alpha Release)

537 lines 23.3 kB
import { EventEmitter } from 'events'; import { writeFile, readFile, mkdir, readdir } from 'fs/promises'; import { join } from 'path'; import { Logger } from '../core/logger.js'; import { ConfigManager } from '../core/config.js'; export class ProjectManager extends EventEmitter { projects = new Map(); projectsPath; logger; config; constructor(projectsPath = './projects', logger, config) { super(); this.projectsPath = projectsPath; this.logger = logger || new Logger({ level: 'info', format: 'text', destination: 'console' }); this.config = config || ConfigManager.getInstance(); } async initialize() { try { await mkdir(this.projectsPath, { recursive: true }); await this.loadProjects(); this.logger.info('Project Manager initialized successfully'); } catch (error) { this.logger.error('Failed to initialize Project Manager', { error }); throw error; } } async createProject(projectData) { const project = { id: projectData.id || `project-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, name: projectData.name || 'Unnamed Project', description: projectData.description || '', type: projectData.type || 'custom', status: 'planning', priority: projectData.priority || 'medium', owner: projectData.owner || 'system', stakeholders: projectData.stakeholders || [], phases: projectData.phases || [], budget: projectData.budget || { total: 0, spent: 0, remaining: 0, currency: 'USD', }, timeline: { plannedStart: projectData.timeline?.plannedStart || new Date(), plannedEnd: projectData.timeline?.plannedEnd || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days actualStart: projectData.timeline?.actualStart, actualEnd: projectData.timeline?.actualEnd, }, tags: projectData.tags || [], metadata: projectData.metadata || {}, createdAt: new Date(), updatedAt: new Date(), auditLog: [], collaboration: { teamMembers: [], communication: [], sharedResources: [], }, qualityGates: [], complianceRequirements: [], }; // Add initial audit entry this.addAuditEntry(project, 'system', 'project_created', 'project', { projectId: project.id, projectName: project.name, }); this.projects.set(project.id, project); await this.saveProject(project); this.emit('project:created', project); this.logger.info(`Project created: ${project.name} (${project.id})`); return project; } async updateProject(projectId, updates) { const project = this.projects.get(projectId); if (!project) { throw new Error(`Project not found: ${projectId}`); } const updatedProject = { ...project, ...updates, updatedAt: new Date() }; // Add audit entry this.addAuditEntry(updatedProject, 'system', 'project_updated', 'project', { projectId, changes: Object.keys(updates), }); this.projects.set(projectId, updatedProject); await this.saveProject(updatedProject); this.emit('project:updated', updatedProject); this.logger.info(`Project updated: ${project.name} (${projectId})`); return updatedProject; } async deleteProject(projectId, userId = 'system') { const project = this.projects.get(projectId); if (!project) { throw new Error(`Project not found: ${projectId}`); } // Add audit entry before deletion this.addAuditEntry(project, userId, 'project_deleted', 'project', { projectId, projectName: project.name, }); this.projects.delete(projectId); // Archive project instead of deleting const archivePath = join(this.projectsPath, 'archived'); await mkdir(archivePath, { recursive: true }); await writeFile(join(archivePath, `${projectId}.json`), JSON.stringify(project, null, 2)); this.emit('project:deleted', { projectId, project }); this.logger.info(`Project archived: ${project.name} (${projectId})`); } async getProject(projectId) { return this.projects.get(projectId) || null; } async listProjects(filters) { let projects = Array.from(this.projects.values()); if (filters) { if (filters.status) { projects = projects.filter((p) => p.status === filters.status); } if (filters.type) { projects = projects.filter((p) => p.type === filters.type); } if (filters.priority) { projects = projects.filter((p) => p.priority === filters.priority); } if (filters.owner) { projects = projects.filter((p) => p.owner === filters.owner); } if (filters.tags && filters.tags.length > 0) { projects = projects.filter((p) => filters.tags.some((tag) => p.tags.includes(tag))); } } return projects.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); } async addPhase(projectId, phase) { const project = this.projects.get(projectId); if (!project) { throw new Error(`Project not found: ${projectId}`); } const newPhase = { id: `phase-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, ...phase, }; project.phases.push(newPhase); project.updatedAt = new Date(); this.addAuditEntry(project, 'system', 'phase_added', 'phase', { projectId, phaseId: newPhase.id, phaseName: newPhase.name, }); await this.saveProject(project); this.emit('phase:added', { project, phase: newPhase }); return newPhase; } async updatePhase(projectId, phaseId, updates) { const project = this.projects.get(projectId); if (!project) { throw new Error(`Project not found: ${projectId}`); } const phaseIndex = project.phases.findIndex((p) => p.id === phaseId); if (phaseIndex === -1) { throw new Error(`Phase not found: ${phaseId}`); } const updatedPhase = { ...project.phases[phaseIndex], ...updates }; project.phases[phaseIndex] = updatedPhase; project.updatedAt = new Date(); this.addAuditEntry(project, 'system', 'phase_updated', 'phase', { projectId, phaseId, changes: Object.keys(updates), }); await this.saveProject(project); this.emit('phase:updated', { project, phase: updatedPhase }); return updatedPhase; } async addTeamMember(projectId, member) { const project = this.projects.get(projectId); if (!project) { throw new Error(`Project not found: ${projectId}`); } project.collaboration.teamMembers.push(member); project.updatedAt = new Date(); this.addAuditEntry(project, 'system', 'team_member_added', 'team', { projectId, memberId: member.id, memberName: member.name, }); await this.saveProject(project); this.emit('team:member_added', { project, member }); } async removeTeamMember(projectId, memberId) { const project = this.projects.get(projectId); if (!project) { throw new Error(`Project not found: ${projectId}`); } const memberIndex = project.collaboration.teamMembers.findIndex((m) => m.id === memberId); if (memberIndex === -1) { throw new Error(`Team member not found: ${memberId}`); } const member = project.collaboration.teamMembers[memberIndex]; project.collaboration.teamMembers.splice(memberIndex, 1); project.updatedAt = new Date(); this.addAuditEntry(project, 'system', 'team_member_removed', 'team', { projectId, memberId, memberName: member.name, }); await this.saveProject(project); this.emit('team:member_removed', { project, memberId }); } async getProjectMetrics(projectId) { const projects = projectId ? [this.projects.get(projectId)].filter(Boolean) : Array.from(this.projects.values()); const totalProjects = projects.length; const activeProjects = projects.filter((p) => p.status === 'active').length; const completedProjects = projects.filter((p) => p.status === 'completed').length; const completedProjectsWithDuration = projects.filter((p) => p.status === 'completed' && p.timeline.actualStart && p.timeline.actualEnd); const averageProjectDuration = completedProjectsWithDuration.length > 0 ? completedProjectsWithDuration.reduce((sum, p) => { const duration = p.timeline.actualEnd.getTime() - p.timeline.actualStart.getTime(); return sum + duration / (1000 * 60 * 60 * 24); // Convert to days }, 0) / completedProjectsWithDuration.length : 0; const budgetVariance = projects.reduce((sum, p) => { if (p.budget.total > 0) { return sum + (p.budget.spent - p.budget.total) / p.budget.total; } return sum; }, 0) / Math.max(projects.length, 1); const resourceUtilization = projects.reduce((sum, p) => { const totalResources = p.phases.reduce((phaseSum, phase) => phaseSum + phase.resources.length, 0); const utilizedResources = p.phases.reduce((phaseSum, phase) => phaseSum + phase.resources.filter((r) => r.availability > 0).length, 0); return sum + (totalResources > 0 ? utilizedResources / totalResources : 0); }, 0) / Math.max(projects.length, 1); const qualityScore = projects.reduce((sum, p) => { const phaseQuality = p.phases.reduce((phaseSum, phase) => { const metrics = phase.qualityMetrics; return (phaseSum + (metrics.testCoverage + metrics.codeQuality + metrics.documentation + metrics.securityScore) / 4); }, 0) / Math.max(p.phases.length, 1); return sum + phaseQuality; }, 0) / Math.max(projects.length, 1); return { totalProjects, activeProjects, completedProjects, averageProjectDuration, budgetVariance, resourceUtilization, qualityScore, riskScore: 0, // Calculate based on risk assessment teamProductivity: 0, // Calculate based on velocity metrics customerSatisfaction: 0, // Calculate based on feedback }; } async generateReport(projectId, type, userId = 'system') { const project = this.projects.get(projectId); if (!project) { throw new Error(`Project not found: ${projectId}`); } const report = { id: `report-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, projectId, type, title: `${type.toUpperCase()} Report - ${project.name}`, summary: '', details: {}, recommendations: [], generatedAt: new Date(), generatedBy: userId, format: 'json', recipients: [], }; switch (type) { case 'status': report.summary = `Project ${project.name} is currently ${project.status}`; report.details = { status: project.status, progress: this.calculateProjectProgress(project), phases: project.phases.map((p) => ({ name: p.name, status: p.status, completion: p.completionPercentage, })), timeline: project.timeline, nextMilestones: this.getUpcomingMilestones(project), }; break; case 'financial': report.summary = `Budget utilization: ${((project.budget.spent / project.budget.total) * 100).toFixed(1)}%`; report.details = { budget: project.budget, costBreakdown: this.calculateCostBreakdown(project), variance: project.budget.spent - project.budget.total, projectedCost: this.projectFinalCost(project), }; break; case 'quality': const qualityMetrics = this.calculateQualityMetrics(project); report.summary = `Overall quality score: ${qualityMetrics.overall.toFixed(1)}%`; report.details = { qualityMetrics, qualityGates: project.qualityGates, recommendations: this.generateQualityRecommendations(project), }; break; case 'risk': const risks = this.getAllRisks(project); report.summary = `${risks.filter((r) => r.status === 'open').length} open risks identified`; report.details = { risks, riskMatrix: this.generateRiskMatrix(risks), mitigation: this.generateRiskMitigation(risks), }; break; case 'resource': report.summary = `${project.collaboration.teamMembers.length} team members, ${this.getTotalResources(project)} resources allocated`; report.details = { teamMembers: project.collaboration.teamMembers, resourceAllocation: this.calculateResourceAllocation(project), utilization: this.calculateResourceUtilization(project), capacity: this.calculateCapacity(project), }; break; case 'compliance': const compliance = this.calculateComplianceStatus(project); report.summary = `${compliance.compliant} of ${compliance.total} requirements met`; report.details = { requirements: project.complianceRequirements, status: compliance, gaps: this.identifyComplianceGaps(project), recommendations: this.generateComplianceRecommendations(project), }; break; } this.addAuditEntry(project, userId, 'report_generated', 'report', { projectId, reportId: report.id, reportType: type, }); this.emit('report:generated', { project, report }); return report; } async loadProjects() { try { const files = await readdir(this.projectsPath); const projectFiles = files.filter((f) => f.endsWith('.json') && !f.startsWith('.')); for (const file of projectFiles) { try { const content = await readFile(join(this.projectsPath, file), 'utf-8'); const project = JSON.parse(content); this.projects.set(project.id, project); } catch (error) { this.logger.error(`Failed to load project file: ${file}`, { error }); } } this.logger.info(`Loaded ${this.projects.size} projects`); } catch (error) { this.logger.error('Failed to load projects', { error }); } } async saveProject(project) { const filePath = join(this.projectsPath, `${project.id}.json`); await writeFile(filePath, JSON.stringify(project, null, 2)); } addAuditEntry(project, userId, action, target, details) { const entry = { id: `audit-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, timestamp: new Date(), userId, action, target, details, }; project.auditLog.push(entry); } calculateProjectProgress(project) { if (project.phases.length === 0) return 0; const totalProgress = project.phases.reduce((sum, phase) => sum + phase.completionPercentage, 0); return totalProgress / project.phases.length; } getUpcomingMilestones(project) { const allMilestones = project.phases.flatMap((p) => p.milestones); const now = new Date(); return allMilestones .filter((m) => m.status === 'pending' && m.targetDate > now) .sort((a, b) => a.targetDate.getTime() - b.targetDate.getTime()) .slice(0, 5); } calculateCostBreakdown(project) { const breakdown = {}; for (const phase of project.phases) { for (const resource of phase.resources) { const category = resource.type; const cost = resource.cost.amount; breakdown[category] = (breakdown[category] || 0) + cost; } } return breakdown; } projectFinalCost(project) { const progress = this.calculateProjectProgress(project); if (progress === 0) return project.budget.total; return (project.budget.spent / progress) * 100; } calculateQualityMetrics(project) { const allMetrics = project.phases.map((p) => p.qualityMetrics); if (allMetrics.length === 0) { return { overall: 0, testCoverage: 0, codeQuality: 0, documentation: 0, securityScore: 0 }; } const averages = { testCoverage: allMetrics.reduce((sum, m) => sum + m.testCoverage, 0) / allMetrics.length, codeQuality: allMetrics.reduce((sum, m) => sum + m.codeQuality, 0) / allMetrics.length, documentation: allMetrics.reduce((sum, m) => sum + m.documentation, 0) / allMetrics.length, securityScore: allMetrics.reduce((sum, m) => sum + m.securityScore, 0) / allMetrics.length, }; const overall = (averages.testCoverage + averages.codeQuality + averages.documentation + averages.securityScore) / 4; return { overall, ...averages }; } generateQualityRecommendations(project) { const recommendations = []; const metrics = this.calculateQualityMetrics(project); if (metrics.testCoverage < 80) { recommendations.push('Increase test coverage to at least 80%'); } if (metrics.codeQuality < 70) { recommendations.push('Improve code quality through refactoring and code reviews'); } if (metrics.documentation < 60) { recommendations.push('Enhance documentation coverage for better maintainability'); } if (metrics.securityScore < 85) { recommendations.push('Address security vulnerabilities and implement security best practices'); } return recommendations; } getAllRisks(project) { return project.phases.flatMap((p) => p.risks); } generateRiskMatrix(risks) { const matrix = { low: { low: 0, medium: 0, high: 0 }, medium: { low: 0, medium: 0, high: 0 }, high: { low: 0, medium: 0, high: 0 }, }; for (const risk of risks) { if (risk.status === 'open') { matrix[risk.probability][risk.impact]++; } } return matrix; } generateRiskMitigation(risks) { const openRisks = risks.filter((r) => r.status === 'open'); const highPriorityRisks = openRisks.filter((r) => (r.probability === 'high' && r.impact === 'high') || (r.probability === 'high' && r.impact === 'medium') || (r.probability === 'medium' && r.impact === 'high')); return { totalRisks: risks.length, openRisks: openRisks.length, highPriorityRisks: highPriorityRisks.length, mitigationActions: highPriorityRisks.map((r) => ({ risk: r.description, mitigation: r.mitigation, assignedTo: r.assignedTo, })), }; } getTotalResources(project) { return project.phases.reduce((sum, phase) => sum + phase.resources.length, 0); } calculateResourceAllocation(project) { const allocation = {}; for (const phase of project.phases) { for (const resource of phase.resources) { allocation[resource.type] = (allocation[resource.type] || 0) + 1; } } return allocation; } calculateResourceUtilization(project) { const utilization = {}; for (const phase of project.phases) { for (const resource of phase.resources) { utilization[resource.type] = (utilization[resource.type] || 0) + resource.availability; } } return utilization; } calculateCapacity(project) { const teamSize = project.collaboration.teamMembers.length; const totalAvailability = project.collaboration.teamMembers.reduce((sum, member) => sum + member.availability, 0); return { teamSize, totalAvailability, averageAvailability: teamSize > 0 ? totalAvailability / teamSize : 0, }; } calculateComplianceStatus(project) { const requirements = project.complianceRequirements; const total = requirements.length; const compliant = requirements.filter((r) => r.status === 'compliant').length; const inProgress = requirements.filter((r) => r.status === 'in-progress').length; const nonCompliant = requirements.filter((r) => r.status === 'non-compliant').length; return { total, compliant, inProgress, nonCompliant, compliancePercentage: total > 0 ? (compliant / total) * 100 : 0, }; } identifyComplianceGaps(project) { return project.complianceRequirements.filter((r) => r.status === 'not-started' || r.status === 'non-compliant'); } generateComplianceRecommendations(project) { const gaps = this.identifyComplianceGaps(project); const recommendations = []; for (const gap of gaps) { recommendations.push(`Address ${gap.framework} requirement: ${gap.name} (Due: ${gap.dueDate.toLocaleDateString()})`); } return recommendations; } } //# sourceMappingURL=project-manager.js.map