const fs = require('fs'); const path = require('path'); class ContextManager { constructor(workspacePath = null) { this.workspacePath = workspacePath || path.join(process.cwd(), '.workspace'); this.contextPath = path.join(this.workspacePath, 'context'); this.decisionsPath = path.join(this.workspacePath, 'decisions'); this.progressPath = path.join(this.workspacePath, 'progress'); this.qualityPath = path.join(this.workspacePath, 'quality'); this.archivePath = path.join(this.workspacePath, 'archive'); this.versionsPath = path.join(this.workspacePath, 'versions'); this.locksPath = path.join(this.workspacePath, 'locks'); // Context file size threshold for compaction (10MB default) this.maxContextSize = 10 * 1024 * 1024; // Context versioning settings this.maxVersions = 50; // Keep last 50 versions this.conflictResolutionStrategy = 'merge'; // 'merge', 'overwrite', 'manual' // Initialize if needed this.initialize(); } initialize() { // Ensure all context directories exist const dirs = [this.contextPath, this.decisionsPath, this.progressPath, this.qualityPath, this.archivePath, this.versionsPath, this.locksPath]; for (const dir of dirs) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } } // SHARED CONTEXT MANAGEMENT async updateSharedContext(updates) { try { const contextFile = path.join(this.contextPath, 'shared-context.md'); let context = await this.loadSharedContext(); // Merge updates with existing context const updatedContext = { ...context, ...updates, lastUpdated: new Date().toISOString() }; const contextContent = this.formatSharedContext(updatedContext); fs.writeFileSync(contextFile, contextContent); // Check if compaction is needed await this.checkContextCompaction(contextFile); return updatedContext; } catch (error) { console.error('Failed to update shared context:', error.message); throw error; } } async loadSharedContext() { try { const contextFile = path.join(this.contextPath, 'shared-context.md'); if (!fs.existsSync(contextFile)) { return this.getDefaultSharedContext(); } const content = fs.readFileSync(contextFile, 'utf8'); return this.parseSharedContext(content); } catch (error) { console.error('Failed to load shared context:', error.message); return this.getDefaultSharedContext(); } } getDefaultSharedContext() { return { lastUpdated: new Date().toISOString(), activeSessions: [], primaryAgent: 'unknown', currentFocus: 'No active development focus', keyDecisions: [], nextSteps: [], sessionNotes: '' }; } formatSharedContext(context) { return `# Workspace Context **Last Updated:** ${context.lastUpdated} **Active Sessions:** ${context.activeSessions.join(', ') || 'None'} **Primary Agent:** ${context.primaryAgent} ## Current Focus ${context.currentFocus} ## Key Decisions ${context.keyDecisions.map(decision => `- ${decision}`).join('\n') || '- No decisions recorded yet'} ## Next Steps ${context.nextSteps.map(step => `- ${step}`).join('\n') || '- No next steps defined yet'} ## Session Notes ${context.sessionNotes || 'No session notes available'} `; } parseSharedContext(content) { const context = this.getDefaultSharedContext(); try { // Extract metadata const lastUpdatedMatch = content.match(/\*\*Last Updated:\*\* (.+)/); if (lastUpdatedMatch) context.lastUpdated = lastUpdatedMatch[1]; const activeSessionsMatch = content.match(/\*\*Active Sessions:\*\* (.+)/); if (activeSessionsMatch && activeSessionsMatch[1] !== 'None') { context.activeSessions = activeSessionsMatch[1].split(', ').map(s => s.trim()); } const primaryAgentMatch = content.match(/\*\*Primary Agent:\*\* (.+)/); if (primaryAgentMatch) context.primaryAgent = primaryAgentMatch[1]; // Extract sections const currentFocusMatch = content.match(/## Current Focus\n([\s\S]*?)(?=\n## |$)/); if (currentFocusMatch) context.currentFocus = currentFocusMatch[1].trim(); const keyDecisionsMatch = content.match(/## Key Decisions\n([\s\S]*?)(?=\n## |$)/); if (keyDecisionsMatch) { context.keyDecisions = keyDecisionsMatch[1] .split('\n') .filter(line => line.startsWith('- ')) .map(line => line.substring(2).trim()) .filter(decision => decision && !decision.includes('No decisions recorded')); } const nextStepsMatch = content.match(/## Next Steps\n([\s\S]*?)(?=\n## |$)/); if (nextStepsMatch) { context.nextSteps = nextStepsMatch[1] .split('\n') .filter(line => line.startsWith('- ')) .map(line => line.substring(2).trim()) .filter(step => step && !step.includes('No next steps defined')); } const sessionNotesMatch = content.match(/## Session Notes\n([\s\S]*?)$/); if (sessionNotesMatch) context.sessionNotes = sessionNotesMatch[1].trim(); } catch (error) { console.warn('Failed to parse shared context, using defaults:', error.message); } return context; } // DECISIONS LOGGING async logDecision(decision) { try { const decisionsFile = path.join(this.decisionsPath, 'decisions-log.md'); let existingContent = ''; if (fs.existsSync(decisionsFile)) { existingContent = fs.readFileSync(decisionsFile, 'utf8'); } else { existingContent = '# Architectural & Design Decisions\n\n'; } // Generate decision ID const decisionCount = (existingContent.match(/## Decision \d+:/g) || []).length; const decisionId = String(decisionCount + 1).padStart(3, '0'); const decisionEntry = `## Decision ${decisionId}: ${decision.title} **Date:** ${decision.date || new Date().toISOString()} **Agent:** ${decision.agent || 'unknown'} **Context:** ${decision.context || 'No context provided'} **Decision:** ${decision.decision} **Rationale:** ${decision.rationale || 'No rationale provided'} **Alternatives:** ${decision.alternatives || 'No alternatives considered'} **Impact:** ${decision.impact || 'Impact not assessed'} **Status:** ${decision.status || 'active'} `; const updatedContent = existingContent + decisionEntry; fs.writeFileSync(decisionsFile, updatedContent); // Update shared context with new decision const context = await this.loadSharedContext(); context.keyDecisions.push(`${decision.title} (${decision.date || new Date().toISOString().split('T')[0]})`); await this.updateSharedContext(context); return decisionId; } catch (error) { console.error('Failed to log decision:', error.message); throw error; } } async getDecisions(filters = {}) { try { const decisionsFile = path.join(this.decisionsPath, 'decisions-log.md'); if (!fs.existsSync(decisionsFile)) { return []; } const content = fs.readFileSync(decisionsFile, 'utf8'); const decisions = this.parseDecisions(content); // Apply filters let filteredDecisions = decisions; if (filters.agent) { filteredDecisions = filteredDecisions.filter(d => d.agent === filters.agent); } if (filters.status) { filteredDecisions = filteredDecisions.filter(d => d.status === filters.status); } if (filters.dateFrom) { const fromDate = new Date(filters.dateFrom); filteredDecisions = filteredDecisions.filter(d => new Date(d.date) >= fromDate); } if (filters.dateTo) { const toDate = new Date(filters.dateTo); filteredDecisions = filteredDecisions.filter(d => new Date(d.date) <= toDate); } return filteredDecisions; } catch (error) { console.error('Failed to get decisions:', error.message); return []; } } parseDecisions(content) { const decisions = []; const decisionBlocks = content.split(/## Decision \d+:/); for (let i = 1; i < decisionBlocks.length; i++) { try { const block = decisionBlocks[i]; const lines = block.split('\n'); const decision = { id: `${i.toString().padStart(3, '0')}`, title: lines[0].trim(), date: this.extractField(block, 'Date'), agent: this.extractField(block, 'Agent'), context: this.extractField(block, 'Context'), decision: this.extractField(block, 'Decision'), rationale: this.extractField(block, 'Rationale'), alternatives: this.extractField(block, 'Alternatives'), impact: this.extractField(block, 'Impact'), status: this.extractField(block, 'Status') }; decisions.push(decision); } catch (error) { console.warn(`Failed to parse decision block ${i}:`, error.message); } } return decisions; } extractField(content, fieldName) { const regex = new RegExp(`\\*\\*${fieldName}:\\*\\* (.+)`, 'i'); const match = content.match(regex); return match ? match[1].trim() : ''; } // PROGRESS TRACKING async updateProgress(progressUpdate) { try { const progressFile = path.join(this.progressPath, 'progress-summary.md'); let progress = await this.loadProgress(); // Merge updates const updatedProgress = { ...progress, ...progressUpdate, lastUpdated: new Date().toISOString() }; const progressContent = this.formatProgress(updatedProgress); fs.writeFileSync(progressFile, progressContent); // Update shared context const context = await this.loadSharedContext(); if (progressUpdate.currentStory) { context.currentFocus = progressUpdate.currentStory; } if (progressUpdate.nextSteps) { context.nextSteps = progressUpdate.nextSteps; } await this.updateSharedContext(context); return updatedProgress; } catch (error) { console.error('Failed to update progress:', error.message); throw error; } } async loadProgress() { try { const progressFile = path.join(this.progressPath, 'progress-summary.md'); if (!fs.existsSync(progressFile)) { return this.getDefaultProgress(); } const content = fs.readFileSync(progressFile, 'utf8'); return this.parseProgress(content); } catch (error) { console.error('Failed to load progress:', error.message); return this.getDefaultProgress(); } } getDefaultProgress() { return { lastUpdated: new Date().toISOString(), currentStory: 'No active story', completedTasks: [], pendingTasks: [], blockers: [], qualityScore: 'Not assessed' }; } formatProgress(progress) { return `# Development Progress Summary **Last Updated:** ${progress.lastUpdated} **Current Story:** ${progress.currentStory} **Quality Score:** ${progress.qualityScore} ## Completed Tasks ${progress.completedTasks.map(task => `- ✅ ${task}`).join('\n') || '- No tasks completed yet'} ## Pending Tasks ${progress.pendingTasks.map(task => `- ⏳ ${task}`).join('\n') || '- No pending tasks'} ## Blockers ${progress.blockers.map(blocker => `- 🚫 ${blocker}`).join('\n') || '- No blockers identified'} `; } parseProgress(content) { const progress = this.getDefaultProgress(); try { // Extract metadata const lastUpdatedMatch = content.match(/\*\*Last Updated:\*\* (.+)/); if (lastUpdatedMatch) progress.lastUpdated = lastUpdatedMatch[1]; const currentStoryMatch = content.match(/\*\*Current Story:\*\* (.+)/); if (currentStoryMatch) progress.currentStory = currentStoryMatch[1]; const qualityScoreMatch = content.match(/\*\*Quality Score:\*\* (.+)/); if (qualityScoreMatch) progress.qualityScore = qualityScoreMatch[1]; // Extract task lists const completedMatch = content.match(/## Completed Tasks\n([\s\S]*?)(?=\n## |$)/); if (completedMatch) { progress.completedTasks = completedMatch[1] .split('\n') .filter(line => line.startsWith('- ✅')) .map(line => line.substring(4).trim()) .filter(task => task && !task.includes('No tasks completed')); } const pendingMatch = content.match(/## Pending Tasks\n([\s\S]*?)(?=\n## |$)/); if (pendingMatch) { progress.pendingTasks = pendingMatch[1] .split('\n') .filter(line => line.startsWith('- ⏳')) .map(line => line.substring(4).trim()) .filter(task => task && !task.includes('No pending tasks')); } const blockersMatch = content.match(/## Blockers\n([\s\S]*?)$/); if (blockersMatch) { progress.blockers = blockersMatch[1] .split('\n') .filter(line => line.startsWith('- 🚫')) .map(line => line.substring(4).trim()) .filter(blocker => blocker && !blocker.includes('No blockers')); } } catch (error) { console.warn('Failed to parse progress, using defaults:', error.message); } return progress; } // QUALITY METRICS async updateQualityMetrics(metrics) { try { const qualityFile = path.join(this.qualityPath, 'quality-metrics.md'); const timestamp = new Date().toISOString(); let existingContent = ''; if (fs.existsSync(qualityFile)) { existingContent = fs.readFileSync(qualityFile, 'utf8'); } else { existingContent = '# Quality Metrics History\n\n'; } const metricsEntry = `## Quality Assessment - ${timestamp} **Agent:** ${metrics.agent || 'unknown'} **Story:** ${metrics.story || 'unknown'} **Reality Audit Score:** ${metrics.realityAuditScore || 'N/A'} **Pattern Compliance:** ${metrics.patternCompliance || 'N/A'} **Technical Debt Score:** ${metrics.technicalDebtScore || 'N/A'} **Overall Quality:** ${metrics.overallQuality || 'N/A'} **Details:** ${metrics.details || 'No additional details provided'} --- `; const updatedContent = existingContent + metricsEntry; fs.writeFileSync(qualityFile, updatedContent); // Update progress with quality score const progress = await this.loadProgress(); progress.qualityScore = metrics.overallQuality || metrics.realityAuditScore || 'Updated'; await this.updateProgress(progress); return metrics; } catch (error) { console.error('Failed to update quality metrics:', error.message); throw error; } } async getLatestQualityMetrics() { try { const qualityFile = path.join(this.qualityPath, 'quality-metrics.md'); if (!fs.existsSync(qualityFile)) { return null; } const content = fs.readFileSync(qualityFile, 'utf8'); const assessments = content.split('## Quality Assessment -'); if (assessments.length < 2) { return null; } // Get the most recent assessment const latestAssessment = assessments[1]; return { timestamp: latestAssessment.split('\n')[0].trim(), agent: this.extractField(latestAssessment, 'Agent'), story: this.extractField(latestAssessment, 'Story'), realityAuditScore: this.extractField(latestAssessment, 'Reality Audit Score'), patternCompliance: this.extractField(latestAssessment, 'Pattern Compliance'), technicalDebtScore: this.extractField(latestAssessment, 'Technical Debt Score'), overallQuality: this.extractField(latestAssessment, 'Overall Quality'), details: this.extractField(latestAssessment, 'Details') }; } catch (error) { console.error('Failed to get latest quality metrics:', error.message); return null; } } // CONTEXT COMPACTION async checkContextCompaction(filePath) { try { const stats = fs.statSync(filePath); if (stats.size > this.maxContextSize) { await this.compactContext(filePath); } } catch (error) { console.error('Failed to check context compaction:', error.message); } } async compactContext(filePath) { try { const fileName = path.basename(filePath); const archiveFileName = `archived-${Date.now()}-${fileName}`; const archiveFilePath = path.join(this.archivePath, archiveFileName); // Move original to archive const originalContent = fs.readFileSync(filePath, 'utf8'); fs.writeFileSync(archiveFilePath, originalContent); // Create summarized version const summarizedContent = await this.summarizeContext(originalContent, fileName); fs.writeFileSync(filePath, summarizedContent); console.log(`Context compacted: ${fileName} archived as ${archiveFileName}`); } catch (error) { console.error('Failed to compact context:', error.message); } } async summarizeContext(content, fileName) { // Intelligent summarization preserving key decisions and recent activity const timestamp = new Date().toISOString(); if (fileName === 'shared-context.md') { const context = this.parseSharedContext(content); // Keep only most recent 5 decisions and next steps context.keyDecisions = context.keyDecisions.slice(-5); context.nextSteps = context.nextSteps.slice(-5); context.sessionNotes = `[Archived ${timestamp}] Context compacted - full history available in archive.`; return this.formatSharedContext(context); } // For other file types, create a basic summary return `# ${fileName.replace('.md', '')} - Compacted Summary **Compacted:** ${timestamp} **Original Size:** ${content.length} characters This file was automatically compacted to manage size. Full historical context is available in the archive directory. **Recent Activity Summary:** ${content.substring(content.length - 1000)} [Full historical context archived - use archive restoration if detailed history is needed] `; } // SESSION INTEGRATION async onSessionStart(sessionId, agent) { try { const context = await this.loadSharedContext(); if (!context.activeSessions.includes(sessionId)) { context.activeSessions.push(sessionId); } context.primaryAgent = agent; context.sessionNotes = `Session ${sessionId} started by ${agent} at ${new Date().toISOString()}`; await this.updateSharedContext(context); } catch (error) { console.error('Failed to handle session start:', error.message); } } async onSessionEnd(sessionId) { try { const context = await this.loadSharedContext(); context.activeSessions = context.activeSessions.filter(id => id !== sessionId); context.sessionNotes += `\nSession ${sessionId} ended at ${new Date().toISOString()}`; await this.updateSharedContext(context); } catch (error) { console.error('Failed to handle session end:', error.message); } } // UTILITY METHODS async getWorkspaceStatus() { try { const context = await this.loadSharedContext(); const progress = await this.loadProgress(); const latestQuality = await this.getLatestQualityMetrics(); const recentDecisions = await this.getDecisions({ dateFrom: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString() }); return { context, progress, latestQuality, recentDecisions: recentDecisions.slice(-5) }; } catch (error) { console.error('Failed to get workspace status:', error.message); return null; } } async exportContextSummary() { try { const status = await this.getWorkspaceStatus(); return `# Workspace Context Export **Generated:** ${new Date().toISOString()} ## Current Status - **Primary Agent:** ${status.context.primaryAgent} - **Active Sessions:** ${status.context.activeSessions.join(', ') || 'None'} - **Current Focus:** ${status.context.currentFocus} - **Quality Score:** ${status.progress.qualityScore} ## Recent Decisions (Last 7 Days) ${status.recentDecisions.map(d => `- ${d.title} (${d.agent})`).join('\n') || '- No recent decisions'} ## Progress Summary - **Completed Tasks:** ${status.progress.completedTasks.length} - **Pending Tasks:** ${status.progress.pendingTasks.length} - **Blockers:** ${status.progress.blockers.length} ## Next Steps ${status.context.nextSteps.map(step => `- ${step}`).join('\n') || '- No next steps defined'} `; } catch (error) { console.error('Failed to export context summary:', error.message); return 'Error generating context summary'; } } // CONTEXT VERSIONING AND CONFLICT RESOLUTION async createContextVersion(contextType, content, sessionId, agent) { try { const timestamp = new Date().toISOString(); const versionId = `${contextType}-${timestamp.replace(/[:.]/g, '-')}-${sessionId}`; const versionFile = path.join(this.versionsPath, `${versionId}.json`); const version = { id: versionId, contextType, timestamp, sessionId, agent, content, hash: this.generateContentHash(content) }; fs.writeFileSync(versionFile, JSON.stringify(version, null, 2)); // Cleanup old versions await this.cleanupOldVersions(contextType); return versionId; } catch (error) { console.error('Failed to create context version:', error.message); throw error; } } generateContentHash(content) { // Simple hash function for content comparison let hash = 0; if (content.length === 0) return hash; for (let i = 0; i < content.length; i++) { const char = content.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return hash.toString(); } async cleanupOldVersions(contextType) { try { const versionFiles = fs.readdirSync(this.versionsPath) .filter(file => file.startsWith(contextType) && file.endsWith('.json')) .map(file => { const filePath = path.join(this.versionsPath, file); const stats = fs.statSync(filePath); return { file, path: filePath, mtime: stats.mtime }; }) .sort((a, b) => b.mtime - a.mtime); // Keep only the most recent versions if (versionFiles.length > this.maxVersions) { const filesToDelete = versionFiles.slice(this.maxVersions); for (const fileInfo of filesToDelete) { fs.unlinkSync(fileInfo.path); } } } catch (error) { console.error('Failed to cleanup old versions:', error.message); } } async detectContextConflicts(contextType, newContent, sessionId) { try { const currentContextFile = path.join(this.getContextFilePath(contextType)); if (!fs.existsSync(currentContextFile)) { return { hasConflict: false, conflict: null }; } const currentContent = fs.readFileSync(currentContextFile, 'utf8'); const currentHash = this.generateContentHash(currentContent); const newHash = this.generateContentHash(newContent); if (currentHash === newHash) { return { hasConflict: false, conflict: null }; } // Check if there are concurrent modifications const recentVersions = await this.getRecentVersions(contextType, 5); const concurrentVersions = recentVersions.filter(v => v.sessionId !== sessionId && new Date() - new Date(v.timestamp) < 5 * 60 * 1000 // Within last 5 minutes ); if (concurrentVersions.length > 0) { return { hasConflict: true, conflict: { type: 'concurrent_modification', currentContent, newContent, concurrentVersions } }; } return { hasConflict: false, conflict: null }; } catch (error) { console.error('Failed to detect context conflicts:', error.message); return { hasConflict: false, conflict: null }; } } getContextFilePath(contextType) { switch (contextType) { case 'shared-context': return path.join(this.contextPath, 'shared-context.md'); case 'decisions': return path.join(this.decisionsPath, 'decisions-log.md'); case 'progress': return path.join(this.progressPath, 'progress-summary.md'); case 'quality': return path.join(this.qualityPath, 'quality-metrics.md'); default: return path.join(this.contextPath, `${contextType}.md`); } } async getRecentVersions(contextType, limit = 10) { try { const versionFiles = fs.readdirSync(this.versionsPath) .filter(file => file.startsWith(contextType) && file.endsWith('.json')) .map(file => { const filePath = path.join(this.versionsPath, file); const content = JSON.parse(fs.readFileSync(filePath, 'utf8')); return content; }) .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)) .slice(0, limit); return versionFiles; } catch (error) { console.error('Failed to get recent versions:', error.message); return []; } } async mergeContextChanges(contextType, baseContent, changes1, changes2) { try { // Simple merge strategy - combine unique elements switch (contextType) { case 'shared-context': return this.mergeSharedContext(baseContent, changes1, changes2); case 'decisions': return this.mergeDecisions(baseContent, changes1, changes2); case 'progress': return this.mergeProgress(baseContent, changes1, changes2); default: // Default merge - prefer more recent changes return changes2.timestamp > changes1.timestamp ? changes2.content : changes1.content; } } catch (error) { console.error('Failed to merge context changes:', error.message); throw error; } } mergeSharedContext(base, changes1, changes2) { try { const baseCtx = this.parseSharedContext(base); const ctx1 = this.parseSharedContext(changes1.content); const ctx2 = this.parseSharedContext(changes2.content); // Merge strategy: combine unique items, prefer most recent timestamps const merged = { lastUpdated: new Date().toISOString(), activeSessions: [...new Set([...ctx1.activeSessions, ...ctx2.activeSessions])], primaryAgent: ctx2.lastUpdated > ctx1.lastUpdated ? ctx2.primaryAgent : ctx1.primaryAgent, currentFocus: ctx2.lastUpdated > ctx1.lastUpdated ? ctx2.currentFocus : ctx1.currentFocus, keyDecisions: [...new Set([...ctx1.keyDecisions, ...ctx2.keyDecisions])], nextSteps: [...new Set([...ctx1.nextSteps, ...ctx2.nextSteps])], sessionNotes: `${ctx1.sessionNotes}\n${ctx2.sessionNotes}`.trim() }; return this.formatSharedContext(merged); } catch (error) { console.error('Failed to merge shared context:', error.message); return changes2.content; // Fallback to most recent } } mergeDecisions(base, changes1, changes2) { try { // Decisions are append-only, so merge by combining unique decisions const decisions1 = this.parseDecisions(changes1.content); const decisions2 = this.parseDecisions(changes2.content); const allDecisions = [...decisions1, ...decisions2]; const uniqueDecisions = allDecisions.filter((decision, index, self) => index === self.findIndex(d => d.title === decision.title) ); // Sort by date uniqueDecisions.sort((a, b) => new Date(a.date) - new Date(b.date)); let mergedContent = '# Architectural & Design Decisions\n\n'; uniqueDecisions.forEach((decision, index) => { const decisionId = String(index + 1).padStart(3, '0'); mergedContent += `## Decision ${decisionId}: ${decision.title} **Date:** ${decision.date} **Agent:** ${decision.agent} **Context:** ${decision.context} **Decision:** ${decision.decision} **Rationale:** ${decision.rationale} **Alternatives:** ${decision.alternatives} **Impact:** ${decision.impact} **Status:** ${decision.status} `; }); return mergedContent; } catch (error) { console.error('Failed to merge decisions:', error.message); return changes2.content; // Fallback to most recent } } mergeProgress(base, changes1, changes2) { try { const progress1 = this.parseProgress(changes1.content); const progress2 = this.parseProgress(changes2.content); // Merge strategy: combine tasks, keep most recent story and quality score const merged = { lastUpdated: new Date().toISOString(), currentStory: progress2.lastUpdated > progress1.lastUpdated ? progress2.currentStory : progress1.currentStory, completedTasks: [...new Set([...progress1.completedTasks, ...progress2.completedTasks])], pendingTasks: [...new Set([...progress1.pendingTasks, ...progress2.pendingTasks])], blockers: [...new Set([...progress1.blockers, ...progress2.blockers])], qualityScore: progress2.lastUpdated > progress1.lastUpdated ? progress2.qualityScore : progress1.qualityScore }; return this.formatProgress(merged); } catch (error) { console.error('Failed to merge progress:', error.message); return changes2.content; // Fallback to most recent } } async rollbackToVersion(contextType, versionId) { try { const versionFile = path.join(this.versionsPath, `${versionId}.json`); if (!fs.existsSync(versionFile)) { throw new Error(`Version ${versionId} not found`); } const version = JSON.parse(fs.readFileSync(versionFile, 'utf8')); const contextFile = this.getContextFilePath(contextType); // Create backup of current version await this.createContextVersion(contextType, fs.readFileSync(contextFile, 'utf8'), 'system', 'rollback-backup'); // Restore the version fs.writeFileSync(contextFile, version.content); return { success: true, rolledBackTo: version.timestamp, agent: version.agent, sessionId: version.sessionId }; } catch (error) { console.error('Failed to rollback to version:', error.message); throw error; } } // CONTEXT LOCKING FOR CONCURRENT ACCESS async acquireContextLock(contextType, sessionId, timeout = 30000) { try { const lockFile = path.join(this.locksPath, `${contextType}.lock`); const lockInfo = { sessionId, timestamp: new Date().toISOString(), expires: new Date(Date.now() + timeout).toISOString() }; // Check for existing lock if (fs.existsSync(lockFile)) { const existingLock = JSON.parse(fs.readFileSync(lockFile, 'utf8')); // Check if lock has expired if (new Date(existingLock.expires) > new Date()) { if (existingLock.sessionId !== sessionId) { return { acquired: false, lockedBy: existingLock.sessionId, expiresAt: existingLock.expires }; } // Same session, extend the lock lockInfo.timestamp = existingLock.timestamp; } } fs.writeFileSync(lockFile, JSON.stringify(lockInfo, null, 2)); return { acquired: true, lockInfo }; } catch (error) { console.error('Failed to acquire context lock:', error.message); return { acquired: false, error: error.message }; } } async releaseContextLock(contextType, sessionId) { try { const lockFile = path.join(this.locksPath, `${contextType}.lock`); if (!fs.existsSync(lockFile)) { return { released: true, message: 'No lock existed' }; } const existingLock = JSON.parse(fs.readFileSync(lockFile, 'utf8')); if (existingLock.sessionId !== sessionId) { return { released: false, message: 'Lock owned by different session' }; } fs.unlinkSync(lockFile); return { released: true, message: 'Lock released successfully' }; } catch (error) { console.error('Failed to release context lock:', error.message); return { released: false, error: error.message }; } } async cleanupExpiredLocks() { try { const lockFiles = fs.readdirSync(this.locksPath).filter(f => f.endsWith('.lock')); let cleanedCount = 0; for (const lockFile of lockFiles) { const lockPath = path.join(this.locksPath, lockFile); const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); if (new Date(lock.expires) < new Date()) { fs.unlinkSync(lockPath); cleanedCount++; } } return { cleanedCount }; } catch (error) { console.error('Failed to cleanup expired locks:', error.message); return { cleanedCount: 0, error: error.message }; } } // BMAD AGENT INTEGRATION HOOKS async onStoryStart(storyId, agent, sessionId) { try { // Automatically update context when a story starts await this.updateSharedContext({ currentFocus: `Story ${storyId} started by ${agent}`, primaryAgent: agent }); // Log story start as a progress update await this.updateProgress({ currentStory: storyId, lastUpdated: new Date().toISOString() }); console.log(`✅ Context updated for story start: ${storyId} by ${agent}`); } catch (error) { console.error('Failed to handle story start:', error.message); } } async onDecisionMade(decision, agent, sessionId) { try { // Automatically log decisions made during agent operations await this.logDecision({ title: decision.title, agent: agent, context: decision.context || 'Auto-captured during agent operation', decision: decision.decision, rationale: decision.rationale || 'No rationale provided', alternatives: decision.alternatives || 'No alternatives considered', impact: decision.impact || 'Impact not assessed', status: decision.status || 'active' }); console.log(`✅ Decision auto-logged: ${decision.title} by ${agent}`); } catch (error) { console.error('Failed to handle decision made:', error.message); } } async onQualityAudit(results, agent, sessionId) { try { // Automatically capture quality audit results await this.updateQualityMetrics({ agent: agent, story: results.story || 'Unknown story', realityAuditScore: results.realityAuditScore || results.score, patternCompliance: results.patternCompliance, technicalDebtScore: results.technicalDebtScore, overallQuality: results.overallQuality || results.grade, details: results.details || 'Auto-captured quality audit results' }); console.log(`✅ Quality metrics auto-captured: ${results.overallQuality || results.grade} by ${agent}`); } catch (error) { console.error('Failed to handle quality audit:', error.message); } } async onAgentHandoff(fromAgent, toAgent, sessionId, handoffContext) { try { // Automatically update context during agent handoffs await this.updateSharedContext({ primaryAgent: toAgent, sessionNotes: `Agent handoff: ${fromAgent} → ${toAgent} at ${new Date().toISOString()}` }); // Log handoff as a decision if it includes important context if (handoffContext && handoffContext.length > 50) { await this.logDecision({ title: `Agent handoff: ${fromAgent} to ${toAgent}`, agent: fromAgent, context: 'Agent transition', decision: `Handed off work context to ${toAgent}`, rationale: handoffContext, impact: `${toAgent} can continue work with full context`, status: 'active' }); } console.log(`✅ Context updated for agent handoff: ${fromAgent} → ${toAgent}`); } catch (error) { console.error('Failed to handle agent handoff:', error.message); } } } module.exports = ContextManager;