From dd487630ee6eb24d3d69623b76b62bb59be84e56 Mon Sep 17 00:00:00 2001 From: "James (Claude Code)" Date: Wed, 23 Jul 2025 18:38:32 -0400 Subject: [PATCH] feat: Add comprehensive cross-IDE workspace utility system - Add workspace-utils/ directory with 6 comprehensive utility scripts (2600+ lines) - Implement workspace initialization, status, health monitoring, cleanup, handoff, and sync - Add npm scripts integration for Node.js projects with standalone fallback - Create IDE-specific documentation for Cursor, Windsurf, GitHub Copilot - Add context-manager and handoff-manager for workspace coordination - Support cross-IDE collaboration with environment detection and session management - Enable automatic workspace deployment during BMAD installation --- package.json | 6 + tools/installer/lib/context-manager.js | 1059 ++++++++++++++++++++++++ tools/installer/lib/handoff-manager.js | 938 +++++++++++++++++++++ workspace-utils/cleanup.js | 325 ++++++++ workspace-utils/docs/cursor.md | 215 +++++ workspace-utils/docs/github-copilot.md | 388 +++++++++ workspace-utils/docs/windsurf.md | 306 +++++++ workspace-utils/handoff.js | 399 +++++++++ workspace-utils/health.js | 549 ++++++++++++ workspace-utils/init.js | 292 +++++++ workspace-utils/status.js | 259 ++++++ workspace-utils/sync.js | 448 ++++++++++ 12 files changed, 5184 insertions(+) create mode 100644 tools/installer/lib/context-manager.js create mode 100644 tools/installer/lib/handoff-manager.js create mode 100644 workspace-utils/cleanup.js create mode 100644 workspace-utils/docs/cursor.md create mode 100644 workspace-utils/docs/github-copilot.md create mode 100644 workspace-utils/docs/windsurf.md create mode 100644 workspace-utils/handoff.js create mode 100644 workspace-utils/health.js create mode 100644 workspace-utils/init.js create mode 100644 workspace-utils/status.js create mode 100644 workspace-utils/sync.js diff --git a/package.json b/package.json index c16882c9..b978475f 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,12 @@ "validate": "node tools/cli.js validate", "install:bmad": "node tools/installer/bin/bmad.js install", "format": "prettier --write \"**/*.md\"", + "workspace-init": "node workspace-utils/init.js", + "workspace-status": "node workspace-utils/status.js", + "workspace-cleanup": "node workspace-utils/cleanup.js", + "workspace-handoff": "node workspace-utils/handoff.js", + "workspace-sync": "node workspace-utils/sync.js", + "workspace-health": "node workspace-utils/health.js", "version:patch": "node tools/version-bump.js patch", "version:minor": "node tools/version-bump.js minor", "version:major": "node tools/version-bump.js major", diff --git a/tools/installer/lib/context-manager.js b/tools/installer/lib/context-manager.js new file mode 100644 index 00000000..2039f40d --- /dev/null +++ b/tools/installer/lib/context-manager.js @@ -0,0 +1,1059 @@ +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; \ No newline at end of file diff --git a/tools/installer/lib/handoff-manager.js b/tools/installer/lib/handoff-manager.js new file mode 100644 index 00000000..520fcd04 --- /dev/null +++ b/tools/installer/lib/handoff-manager.js @@ -0,0 +1,938 @@ +const fs = require('fs'); +const path = require('path'); + +class HandoffManager { + constructor(workspacePath = null) { + this.workspacePath = workspacePath || path.join(process.cwd(), '.workspace'); + this.handoffsPath = path.join(this.workspacePath, 'handoffs'); + this.contextPath = path.join(this.workspacePath, 'context'); + + // Initialize directories + this.initialize(); + + // Agent-specific filtering rules with multi-role support + this.agentFilters = { + 'dev': { + includePatterns: ['technical', 'implementation', 'code', 'architecture', 'bug', 'feature'], + excludePatterns: ['business', 'stakeholder', 'marketing'], + requiredSections: ['technical details', 'code references', 'implementation requirements'] + }, + 'qa': { + includePatterns: ['testing', 'validation', 'quality', 'acceptance', 'bug', 'criteria'], + excludePatterns: ['implementation details', 'code specifics'], + requiredSections: ['acceptance criteria', 'testing requirements', 'quality standards'] + }, + 'architect': { + includePatterns: ['design', 'architecture', 'system', 'integration', 'technical', 'pattern'], + excludePatterns: ['implementation specifics', 'testing details'], + requiredSections: ['design decisions', 'technical constraints', 'system architecture'] + }, + 'pm': { + includePatterns: ['requirements', 'business', 'stakeholder', 'scope', 'timeline', 'priority'], + excludePatterns: ['technical implementation', 'code details'], + requiredSections: ['business requirements', 'stakeholder decisions', 'scope changes'] + }, + 'ux-expert': { + includePatterns: ['user', 'interface', 'experience', 'design', 'usability', 'interaction'], + excludePatterns: ['backend', 'database', 'server'], + requiredSections: ['user requirements', 'design specifications', 'usability considerations'] + }, + 'analyst': { + includePatterns: ['data', 'analysis', 'metrics', 'trends', 'insights', 'research', 'patterns', 'statistics'], + excludePatterns: ['implementation', 'specific code'], + requiredSections: ['data analysis', 'insights and trends', 'research findings'] + }, + 'brainstorming': { + includePatterns: ['creative', 'ideation', 'brainstorm', 'innovation', 'alternative', 'possibility', 'exploration'], + excludePatterns: ['constraints', 'limitations', 'final decisions'], + requiredSections: ['creative exploration', 'alternative approaches', 'innovative solutions'] + }, + 'research': { + includePatterns: ['research', 'investigation', 'study', 'benchmark', 'industry', 'best-practice', 'standards'], + excludePatterns: ['implementation', 'specific solutions'], + requiredSections: ['research methodology', 'findings and insights', 'recommendations'] + } + }; + + // Multi-role combinations for complex scenarios + this.multiRoleFilters = { + 'dev-analyst': { + primary: 'dev', + secondary: 'analyst', + description: 'Development with data analysis capabilities' + }, + 'qa-research': { + primary: 'qa', + secondary: 'research', + description: 'Quality assurance with research methodologies' + }, + 'architect-brainstorming': { + primary: 'architect', + secondary: 'brainstorming', + description: 'Architecture design with creative exploration' + }, + 'pm-analyst': { + primary: 'pm', + secondary: 'analyst', + description: 'Project management with data analysis' + }, + 'ux-research': { + primary: 'ux-expert', + secondary: 'research', + description: 'UX design with user research capabilities' + } + }; + } + + initialize() { + if (!fs.existsSync(this.handoffsPath)) { + fs.mkdirSync(this.handoffsPath, { recursive: true }); + } + } + + async createHandoff(sourceAgent, targetAgent, context = {}) { + try { + const timestamp = new Date().toISOString(); + const handoffId = `${sourceAgent}-to-${targetAgent}-${timestamp.replace(/[:.]/g, '-')}`; + const handoffFile = path.join(this.handoffsPath, `${handoffId}.md`); + + // Load workspace context using our ContextManager integration + const workspaceContext = await this.loadWorkspaceContext(); + + // Filter context for target agent + const filteredContext = this.filterContextForAgent(workspaceContext, targetAgent); + + // Generate handoff package + const handoffContent = await this.generateHandoffPackage({ + handoffId, + sourceAgent, + targetAgent, + timestamp, + context: filteredContext, + customContext: context + }); + + // Validate handoff completeness + const validation = this.validateHandoff(handoffContent, targetAgent); + + // Write handoff file + fs.writeFileSync(handoffFile, handoffContent); + + // Update handoff registry + await this.updateHandoffRegistry(handoffId, sourceAgent, targetAgent, validation); + + // Log handoff in audit trail + await this.logHandoffEvent({ + handoffId, + sourceAgent, + targetAgent, + timestamp, + status: 'created', + validationScore: validation.score, + filePath: handoffFile + }); + + return { + handoffId, + filePath: handoffFile, + validation, + success: true + }; + + } catch (error) { + console.error('Failed to create handoff:', error.message); + throw error; + } + } + + async loadWorkspaceContext() { + try { + const context = { + shared: {}, + decisions: [], + progress: {}, + quality: {} + }; + + // Load shared context + const sharedContextFile = path.join(this.contextPath, 'shared-context.md'); + if (fs.existsSync(sharedContextFile)) { + context.shared = this.parseSharedContext(fs.readFileSync(sharedContextFile, 'utf8')); + } + + // Load decisions + const decisionsFile = path.join(this.workspacePath, 'decisions', 'decisions-log.md'); + if (fs.existsSync(decisionsFile)) { + context.decisions = this.parseDecisions(fs.readFileSync(decisionsFile, 'utf8')); + } + + // Load progress + const progressFile = path.join(this.workspacePath, 'progress', 'progress-summary.md'); + if (fs.existsSync(progressFile)) { + context.progress = this.parseProgress(fs.readFileSync(progressFile, 'utf8')); + } + + // Load quality metrics + const qualityFile = path.join(this.workspacePath, 'quality', 'quality-metrics.md'); + if (fs.existsSync(qualityFile)) { + context.quality = this.parseQualityMetrics(fs.readFileSync(qualityFile, 'utf8')); + } + + return context; + } catch (error) { + console.error('Failed to load workspace context:', error.message); + return { shared: {}, decisions: [], progress: {}, quality: {} }; + } + } + + parseSharedContext(content) { + const context = {}; + + try { + const lastUpdatedMatch = content.match(/\*\*Last Updated:\*\* (.+)/); + if (lastUpdatedMatch) context.lastUpdated = lastUpdatedMatch[1]; + + const primaryAgentMatch = content.match(/\*\*Primary Agent:\*\* (.+)/); + if (primaryAgentMatch) context.primaryAgent = primaryAgentMatch[1]; + + const currentFocusMatch = content.match(/## Current Focus\n([\s\S]*?)(?=\n## |$)/); + if (currentFocusMatch) context.currentFocus = currentFocusMatch[1].trim(); + + 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.length > 0); + } + + 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:', error.message); + } + + return context; + } + + 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'), + 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.slice(-10); // Last 10 decisions for handoff + } + + parseProgress(content) { + const progress = {}; + + try { + const currentStoryMatch = content.match(/\*\*Current Story:\*\* (.+)/); + if (currentStoryMatch) progress.currentStory = currentStoryMatch[1]; + + const qualityScoreMatch = content.match(/\*\*Quality Score:\*\* (.+)/); + if (qualityScoreMatch) progress.qualityScore = qualityScoreMatch[1]; + + 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.length > 0); + } + + 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.length > 0); + } + + 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.length > 0); + } + + } catch (error) { + console.warn('Failed to parse progress:', error.message); + } + + return progress; + } + + parseQualityMetrics(content) { + const quality = {}; + + try { + // Get the most recent quality assessment + const assessments = content.split('## Quality Assessment -'); + if (assessments.length > 1) { + const latest = assessments[1]; + quality.timestamp = latest.split('\n')[0].trim(); + quality.agent = this.extractField(latest, 'Agent'); + quality.story = this.extractField(latest, 'Story'); + quality.realityAuditScore = this.extractField(latest, 'Reality Audit Score'); + quality.overallQuality = this.extractField(latest, 'Overall Quality'); + } + } catch (error) { + console.warn('Failed to parse quality metrics:', error.message); + } + + return quality; + } + + extractField(content, fieldName) { + const regex = new RegExp(`\\*\\*${fieldName}:\\*\\* (.+)`, 'i'); + const match = content.match(regex); + return match ? match[1].trim() : ''; + } + + filterContextForAgent(context, targetAgent) { + const agentType = this.getAgentType(targetAgent); + + // Handle multi-role filtering + if (this.multiRoleFilters[agentType]) { + return this.filterMultiRoleContext(context, agentType); + } + + // Handle single role filtering + const filter = this.agentFilters[agentType] || this.agentFilters['dev']; // Default to dev filter + + const filtered = { + shared: context.shared, + decisions: this.filterDecisions(context.decisions, filter), + progress: context.progress, + quality: context.quality, + relevantContent: this.extractRelevantContent(context, filter), + roleType: 'single', + primaryRole: agentType + }; + + return filtered; + } + + filterMultiRoleContext(context, multiRoleType) { + const multiRole = this.multiRoleFilters[multiRoleType]; + const primaryFilter = this.agentFilters[multiRole.primary]; + const secondaryFilter = this.agentFilters[multiRole.secondary]; + + // Combine include patterns from both roles + const combinedIncludePatterns = [ + ...primaryFilter.includePatterns, + ...secondaryFilter.includePatterns + ]; + + // Use primary role's exclude patterns but remove conflicts with secondary role + const combinedExcludePatterns = primaryFilter.excludePatterns.filter( + pattern => !secondaryFilter.includePatterns.includes(pattern) + ); + + const combinedFilter = { + includePatterns: combinedIncludePatterns, + excludePatterns: combinedExcludePatterns, + requiredSections: [ + ...primaryFilter.requiredSections, + ...secondaryFilter.requiredSections + ] + }; + + const filtered = { + shared: context.shared, + decisions: this.filterDecisions(context.decisions, combinedFilter), + progress: context.progress, + quality: context.quality, + relevantContent: this.extractRelevantContent(context, combinedFilter), + roleType: 'multi', + primaryRole: multiRole.primary, + secondaryRole: multiRole.secondary, + roleDescription: multiRole.description + }; + + return filtered; + } + + getAgentType(agentName) { + const lowerName = agentName.toLowerCase(); + + // Check for multi-role patterns first (e.g., "dev-analyst", "qa+research") + const multiRolePatterns = [ + { pattern: ['dev', 'analyst'], type: 'dev-analyst' }, + { pattern: ['qa', 'research'], type: 'qa-research' }, + { pattern: ['architect', 'brainstorm'], type: 'architect-brainstorming' }, + { pattern: ['pm', 'analyst'], type: 'pm-analyst' }, + { pattern: ['ux', 'research'], type: 'ux-research' } + ]; + + for (const multiRole of multiRolePatterns) { + if (multiRole.pattern.every(part => lowerName.includes(part))) { + return multiRole.type; + } + } + + // Check for specialized roles + if (lowerName.includes('analyst') || lowerName.includes('analysis')) return 'analyst'; + if (lowerName.includes('brainstorm') || lowerName.includes('creative')) return 'brainstorming'; + if (lowerName.includes('research') || lowerName.includes('investigat')) return 'research'; + + // Check for primary roles + if (lowerName.includes('dev') || lowerName.includes('developer')) return 'dev'; + if (lowerName.includes('qa') || lowerName.includes('test')) return 'qa'; + if (lowerName.includes('arch') || lowerName.includes('architect')) return 'architect'; + if (lowerName.includes('pm') || lowerName.includes('manager')) return 'pm'; + if (lowerName.includes('ux') || lowerName.includes('design')) return 'ux-expert'; + + return 'dev'; // Default fallback + } + + filterDecisions(decisions, filter) { + return decisions.filter(decision => { + const decisionText = `${decision.title} ${decision.decision} ${decision.rationale} ${decision.impact}`.toLowerCase(); + + // Check if decision matches include patterns + const matchesInclude = filter.includePatterns.some(pattern => + decisionText.includes(pattern.toLowerCase()) + ); + + // Check if decision matches exclude patterns + const matchesExclude = filter.excludePatterns.some(pattern => + decisionText.includes(pattern.toLowerCase()) + ); + + return matchesInclude && !matchesExclude; + }); + } + + extractRelevantContent(context, filter) { + const relevant = []; + + // Extract relevant next steps + if (context.shared.nextSteps) { + context.shared.nextSteps.forEach(step => { + const stepText = step.toLowerCase(); + const isRelevant = filter.includePatterns.some(pattern => + stepText.includes(pattern.toLowerCase()) + ); + + if (isRelevant) { + relevant.push(`Next Step: ${step}`); + } + }); + } + + // Extract relevant progress items + if (context.progress.pendingTasks) { + context.progress.pendingTasks.forEach(task => { + const taskText = task.toLowerCase(); + const isRelevant = filter.includePatterns.some(pattern => + taskText.includes(pattern.toLowerCase()) + ); + + if (isRelevant) { + relevant.push(`Pending Task: ${task}`); + } + }); + } + + return relevant; + } + + async generateHandoffPackage(params) { + const { + handoffId, + sourceAgent, + targetAgent, + timestamp, + context, + customContext + } = params; + + const agentType = this.getAgentType(targetAgent); + const nextActions = this.generateNextActions(context, agentType); + const fileReferences = this.generateFileReferences(context); + const blockers = this.extractBlockers(context); + + return `# Agent Handoff: ${sourceAgent} โ†’ ${targetAgent} + +**Created:** ${timestamp} +**Handoff ID:** ${handoffId} +**Source Agent:** ${sourceAgent} +**Target Agent:** ${targetAgent} +**Target Agent Type:** ${agentType} + +## Context Summary +${context.shared.currentFocus || 'No current focus available.'} + +${customContext.summary || ''} + +## Key Decisions Made +${context.decisions.map(d => `- **${d.title}** (${d.agent}, ${d.date}): ${d.decision}`).join('\n') || '- No relevant decisions for this agent type'} + +## Current Progress +**Story:** ${context.progress.currentStory || 'No active story'} +**Quality Score:** ${context.progress.qualityScore || 'Not assessed'} + +**Completed Tasks:** +${context.progress.completedTasks ? context.progress.completedTasks.map(task => `- โœ… ${task}`).join('\n') : '- No completed tasks'} + +**Pending Tasks:** +${context.progress.pendingTasks ? context.progress.pendingTasks.map(task => `- โณ ${task}`).join('\n') : '- No pending tasks'} + +## Next Actions for ${targetAgent} +${nextActions.map(action => `- [ ] ${action}`).join('\n')} + +## Files and References +${fileReferences.join('\n') || '- No specific file references available'} + +## Blockers and Dependencies +${blockers.join('\n') || '- No blockers identified'} + +## Quality Metrics +${context.quality.overallQuality ? `**Latest Quality Score:** ${context.quality.overallQuality}` : 'No quality metrics available'} +${context.quality.story ? `**Last Assessed Story:** ${context.quality.story}` : ''} + +## Relevant Context +${context.relevantContent.map(item => `- ${item}`).join('\n') || '- No additional relevant context'} + +## Handoff Validation +- [ ] Context completeness verified +- [ ] Decisions documented and relevant +- [ ] Next actions clearly defined for ${agentType} role +- [ ] References included +- [ ] Quality metrics current +- [ ] Agent-specific filtering applied +- [ ] Blockers and dependencies identified + +## Handoff Notes +${customContext.notes || 'No additional notes provided.'} + +--- +*Generated by BMAD Agent Handoff System v1.0* +*Handoff Quality Score: ${this.calculateHandoffScore(context, agentType)}/100* +`; + } + + generateNextActions(context, agentType) { + const actions = []; + + // Handle multi-role actions + if (this.multiRoleFilters[agentType]) { + return this.generateMultiRoleActions(context, agentType); + } + + // Handle single-role actions + switch (agentType) { + case 'dev': + actions.push('Review technical requirements and architecture decisions'); + actions.push('Examine current code implementation status'); + actions.push('Address any pending technical tasks or bugs'); + if (context.progress.blockers && context.progress.blockers.length > 0) { + actions.push('Resolve identified blockers and technical dependencies'); + } + break; + + case 'qa': + actions.push('Review acceptance criteria and testing requirements'); + actions.push('Validate completed functionality against requirements'); + actions.push('Execute test cases and identify quality issues'); + actions.push('Update quality metrics and provide feedback'); + break; + + case 'architect': + actions.push('Review system design and architectural decisions'); + actions.push('Validate technical approach and integration points'); + actions.push('Assess scalability and performance implications'); + actions.push('Document any new architectural requirements'); + break; + + case 'pm': + actions.push('Review project scope and timeline status'); + actions.push('Assess stakeholder requirements and priority changes'); + actions.push('Update project planning and resource allocation'); + actions.push('Communicate progress to stakeholders'); + break; + + case 'ux-expert': + actions.push('Review user experience requirements and design specifications'); + actions.push('Validate interface design and usability considerations'); + actions.push('Assess user interaction patterns and feedback'); + actions.push('Update design documentation and prototypes'); + break; + + case 'analyst': + actions.push('Analyze available data and identify key patterns'); + actions.push('Generate insights from metrics and performance data'); + actions.push('Create data visualization and trend analysis'); + actions.push('Provide data-driven recommendations'); + break; + + case 'brainstorming': + actions.push('Explore creative alternatives and innovative approaches'); + actions.push('Generate multiple solution options without constraints'); + actions.push('Challenge existing assumptions and think outside the box'); + actions.push('Facilitate ideation sessions and creative problem-solving'); + break; + + case 'research': + actions.push('Conduct comprehensive research on relevant topics'); + actions.push('Investigate industry best practices and standards'); + actions.push('Gather evidence and benchmark against competitors'); + actions.push('Synthesize research findings into actionable insights'); + break; + + default: + actions.push('Review handoff context and understand current state'); + actions.push('Identify specific tasks relevant to your role'); + actions.push('Address any pending items in your domain'); + } + + // Add context-specific actions + if (context.shared.nextSteps) { + context.shared.nextSteps.forEach(step => { + if (!actions.some(action => action.toLowerCase().includes(step.toLowerCase().substring(0, 20)))) { + actions.push(step); + } + }); + } + + return actions.slice(0, 8); // Limit to 8 actions for readability + } + + generateMultiRoleActions(context, multiRoleType) { + const multiRole = this.multiRoleFilters[multiRoleType]; + const actions = []; + + switch (multiRoleType) { + case 'dev-analyst': + actions.push('Analyze current system performance and identify optimization opportunities'); + actions.push('Review code metrics and technical debt patterns'); + actions.push('Implement data-driven development improvements'); + actions.push('Create performance monitoring and analysis dashboards'); + actions.push('Research and apply evidence-based development practices'); + break; + + case 'qa-research': + actions.push('Research industry testing standards and compliance frameworks'); + actions.push('Investigate best practices for quality assurance methodologies'); + actions.push('Analyze quality trends and benchmark against industry standards'); + actions.push('Design comprehensive testing strategies based on research findings'); + actions.push('Validate testing approaches through evidence-based research'); + break; + + case 'architect-brainstorming': + actions.push('Explore creative architectural patterns and innovative design approaches'); + actions.push('Brainstorm multiple system design alternatives without constraints'); + actions.push('Challenge conventional architecture assumptions'); + actions.push('Generate innovative solutions for complex integration challenges'); + actions.push('Facilitate collaborative design exploration sessions'); + break; + + case 'pm-analyst': + actions.push('Analyze project data to identify trends and optimization opportunities'); + actions.push('Research stakeholder feedback and user behavior patterns'); + actions.push('Create data-driven project prioritization and resource allocation'); + actions.push('Generate insights from project metrics and timeline analysis'); + actions.push('Develop evidence-based project planning and risk assessment'); + break; + + case 'ux-research': + actions.push('Conduct user research and usability studies'); + actions.push('Investigate accessibility standards and inclusive design practices'); + actions.push('Analyze user behavior data and interaction patterns'); + actions.push('Research industry UX trends and best practices'); + actions.push('Validate design decisions through evidence-based user research'); + break; + + default: + actions.push('Apply multi-role perspective to current challenges'); + actions.push('Integrate primary and secondary role capabilities'); + actions.push('Provide comprehensive analysis from multiple viewpoints'); + } + + // Add context-specific actions + if (context.shared.nextSteps) { + context.shared.nextSteps.forEach(step => { + if (!actions.some(action => action.toLowerCase().includes(step.toLowerCase().substring(0, 20)))) { + actions.push(step); + } + }); + } + + return actions.slice(0, 10); // Allow more actions for multi-role scenarios + } + + generateFileReferences(context) { + const references = []; + + // Add standard workspace references + references.push('๐Ÿ“ `.workspace/context/shared-context.md` - Current workspace context'); + references.push('๐Ÿ“‹ `.workspace/decisions/decisions-log.md` - Architectural decisions'); + references.push('๐Ÿ“ˆ `.workspace/progress/progress-summary.md` - Development progress'); + references.push('๐Ÿ“Š `.workspace/quality/quality-metrics.md` - Quality assessments'); + + // Add story-specific references if available + if (context.progress.currentStory) { + references.push(`๐Ÿ“– Story documentation for: ${context.progress.currentStory}`); + } + + return references; + } + + extractBlockers(context) { + const blockers = []; + + if (context.progress.blockers && context.progress.blockers.length > 0) { + context.progress.blockers.forEach(blocker => { + blockers.push(`๐Ÿšซ ${blocker}`); + }); + } + + // Check for decision-based blockers + context.decisions.forEach(decision => { + if (decision.status === 'pending' || decision.impact.toLowerCase().includes('blocker')) { + blockers.push(`โš ๏ธ Decision pending: ${decision.title}`); + } + }); + + return blockers; + } + + validateHandoff(handoffContent, targetAgent) { + const validation = { + score: 0, + maxScore: 100, + issues: [], + strengths: [] + }; + + const agentType = this.getAgentType(targetAgent); + const requiredSections = this.agentFilters[agentType]?.requiredSections || []; + + // Check required sections (30 points) + let sectionsFound = 0; + requiredSections.forEach(section => { + if (handoffContent.toLowerCase().includes(section.toLowerCase())) { + sectionsFound++; + validation.strengths.push(`Required section present: ${section}`); + } else { + validation.issues.push(`Missing required section: ${section}`); + } + }); + + if (requiredSections.length > 0) { + validation.score += (sectionsFound / requiredSections.length) * 30; + } else { + validation.score += 30; // No specific requirements + } + + // Check context completeness (25 points) + const hasContext = handoffContent.includes('## Context Summary') && + handoffContent.length > 500; + if (hasContext) { + validation.score += 25; + validation.strengths.push('Comprehensive context summary provided'); + } else { + validation.issues.push('Context summary incomplete or missing'); + } + + // Check decisions documentation (20 points) + const hasDecisions = handoffContent.includes('## Key Decisions Made'); + if (hasDecisions) { + validation.score += 20; + validation.strengths.push('Key decisions documented'); + } else { + validation.issues.push('Key decisions not documented'); + } + + // Check next actions (15 points) + const hasNextActions = handoffContent.includes('## Next Actions for') && + handoffContent.includes('- [ ]'); + if (hasNextActions) { + validation.score += 15; + validation.strengths.push('Clear next actions defined'); + } else { + validation.issues.push('Next actions unclear or missing'); + } + + // Check references (10 points) + const hasReferences = handoffContent.includes('## Files and References'); + if (hasReferences) { + validation.score += 10; + validation.strengths.push('File references provided'); + } else { + validation.issues.push('File references missing'); + } + + validation.grade = this.scoreToGrade(validation.score); + + return validation; + } + + scoreToGrade(score) { + if (score >= 90) return 'A'; + if (score >= 80) return 'B'; + if (score >= 70) return 'C'; + if (score >= 60) return 'D'; + return 'F'; + } + + calculateHandoffScore(context, agentType) { + let score = 50; // Base score + + // Add points for context richness + if (context.shared.currentFocus) score += 10; + if (context.decisions.length > 0) score += 15; + if (context.progress.currentStory) score += 10; + if (context.quality.overallQuality) score += 10; + if (context.relevantContent.length > 0) score += 5; + + return Math.min(score, 100); + } + + async updateHandoffRegistry(handoffId, sourceAgent, targetAgent, validation) { + try { + const registryFile = path.join(this.handoffsPath, 'handoff-registry.json'); + let registry = []; + + if (fs.existsSync(registryFile)) { + const content = fs.readFileSync(registryFile, 'utf8'); + registry = JSON.parse(content); + } + + registry.push({ + handoffId, + sourceAgent, + targetAgent, + timestamp: new Date().toISOString(), + validationScore: validation.score, + grade: validation.grade, + status: 'pending' + }); + + // Keep only last 100 handoffs + if (registry.length > 100) { + registry = registry.slice(-100); + } + + fs.writeFileSync(registryFile, JSON.stringify(registry, null, 2)); + } catch (error) { + console.error('Failed to update handoff registry:', error.message); + } + } + + async logHandoffEvent(event) { + try { + const auditFile = path.join(this.handoffsPath, 'audit-trail.md'); + let auditContent = ''; + + if (fs.existsSync(auditFile)) { + auditContent = fs.readFileSync(auditFile, 'utf8'); + } else { + auditContent = '# Handoff Audit Trail\n\n'; + } + + const logEntry = `## Handoff ${event.handoffId} +**Timestamp:** ${event.timestamp} +**Source:** ${event.sourceAgent} +**Target:** ${event.targetAgent} +**Status:** ${event.status} +**Validation Score:** ${event.validationScore}/100 +**File:** ${event.filePath} + +--- + +`; + + auditContent += logEntry; + fs.writeFileSync(auditFile, auditContent); + } catch (error) { + console.error('Failed to log handoff event:', error.message); + } + } + + async getPendingHandoffs(targetAgent = null) { + try { + const registryFile = path.join(this.handoffsPath, 'handoff-registry.json'); + + if (!fs.existsSync(registryFile)) { + return []; + } + + const content = fs.readFileSync(registryFile, 'utf8'); + const registry = JSON.parse(content); + + let pending = registry.filter(handoff => handoff.status === 'pending'); + + if (targetAgent) { + pending = pending.filter(handoff => handoff.targetAgent === targetAgent); + } + + return pending.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); + } catch (error) { + console.error('Failed to get pending handoffs:', error.message); + return []; + } + } + + async getHandoffStats() { + try { + const registryFile = path.join(this.handoffsPath, 'handoff-registry.json'); + + if (!fs.existsSync(registryFile)) { + return { total: 0, pending: 0, avgScore: 0, gradeDistribution: {} }; + } + + const content = fs.readFileSync(registryFile, 'utf8'); + const registry = JSON.parse(content); + + const stats = { + total: registry.length, + pending: registry.filter(h => h.status === 'pending').length, + avgScore: registry.reduce((sum, h) => sum + h.validationScore, 0) / registry.length, + gradeDistribution: {} + }; + + // Calculate grade distribution + registry.forEach(handoff => { + stats.gradeDistribution[handoff.grade] = (stats.gradeDistribution[handoff.grade] || 0) + 1; + }); + + return stats; + } catch (error) { + console.error('Failed to get handoff stats:', error.message); + return { total: 0, pending: 0, avgScore: 0, gradeDistribution: {} }; + } + } +} + +module.exports = HandoffManager; \ No newline at end of file diff --git a/workspace-utils/cleanup.js b/workspace-utils/cleanup.js new file mode 100644 index 00000000..741e3002 --- /dev/null +++ b/workspace-utils/cleanup.js @@ -0,0 +1,325 @@ +#!/usr/bin/env node +/** + * BMAD Workspace Cleanup Utility + * Cross-IDE workspace maintenance and optimization + */ + +const fs = require('fs'); +const path = require('path'); + +/** + * Clean up stale sessions + */ +function cleanupStaleSessions(workspacePath) { + const sessionsPath = path.join(workspacePath, 'sessions'); + if (!fs.existsSync(sessionsPath)) { + return { removed: 0, errors: [] }; + } + + const sessionFiles = fs.readdirSync(sessionsPath).filter(f => f.endsWith('.json')); + const now = new Date(); + let removed = 0; + const errors = []; + + for (const file of sessionFiles) { + try { + const sessionPath = path.join(sessionsPath, file); + const sessionContent = fs.readFileSync(sessionPath, 'utf8'); + const sessionData = JSON.parse(sessionContent); + + // Remove sessions older than 24 hours without heartbeat + const lastHeartbeat = new Date(sessionData.lastHeartbeat); + const timeDiff = now - lastHeartbeat; + const isStale = timeDiff > 86400000; // 24 hours + + if (isStale) { + fs.unlinkSync(sessionPath); + removed++; + console.log(`๐Ÿ—‘๏ธ Removed stale session: ${sessionData.id} (${sessionData.ide})`); + } + + } catch (e) { + errors.push(`Failed to process ${file}: ${e.message}`); + // Try to remove corrupted files + try { + fs.unlinkSync(path.join(sessionsPath, file)); + console.log(`๐Ÿ—‘๏ธ Removed corrupted session file: ${file}`); + removed++; + } catch (removeError) { + console.error(`โŒ Could not remove corrupted file ${file}: ${removeError.message}`); + } + } + } + + return { removed, errors }; +} + +/** + * Repair workspace directory structure + */ +function repairWorkspaceStructure(workspacePath) { + const requiredDirs = [ + 'sessions', + 'context', + 'handoffs', + 'decisions', + 'progress', + 'quality', + 'archive', + 'hooks', + 'templates', + 'logs' + ]; + + let created = 0; + + for (const dir of requiredDirs) { + const dirPath = path.join(workspacePath, dir); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + console.log(`๐Ÿ“ Created directory: ${dir}`); + created++; + } + } + + return created; +} + +/** + * Archive old logs + */ +function archiveLogs(workspacePath) { + const logsPath = path.join(workspacePath, 'logs'); + const logFile = path.join(logsPath, 'workspace.log'); + + if (!fs.existsSync(logFile)) { + return { archived: false, reason: 'No log file found' }; + } + + const stats = fs.statSync(logFile); + const logSizeMB = stats.size / (1024 * 1024); + + // Archive logs larger than 5MB + if (logSizeMB > 5) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const archivePath = path.join(workspacePath, 'archive', `workspace-${timestamp}.log`); + + try { + // Ensure archive directory exists + const archiveDir = path.join(workspacePath, 'archive'); + if (!fs.existsSync(archiveDir)) { + fs.mkdirSync(archiveDir, { recursive: true }); + } + + // Move log to archive + fs.renameSync(logFile, archivePath); + + // Create new empty log file + fs.writeFileSync(logFile, ''); + + console.log(`๐Ÿ“ฆ Archived log file: ${logSizeMB.toFixed(1)}MB โ†’ archive/workspace-${timestamp}.log`); + return { archived: true, size: logSizeMB, archivePath }; + + } catch (error) { + console.error(`โŒ Failed to archive log: ${error.message}`); + return { archived: false, reason: error.message }; + } + } + + return { archived: false, reason: `Log size OK (${logSizeMB.toFixed(1)}MB)` }; +} + +/** + * Clean up empty context files + */ +function cleanupContextFiles(workspacePath) { + const contextPath = path.join(workspacePath, 'context'); + if (!fs.existsSync(contextPath)) { + return { removed: 0 }; + } + + let removed = 0; + const files = fs.readdirSync(contextPath); + + for (const file of files) { + const filePath = path.join(contextPath, file); + const stats = fs.statSync(filePath); + + if (stats.isFile() && stats.size === 0) { + fs.unlinkSync(filePath); + console.log(`๐Ÿ—‘๏ธ Removed empty context file: ${file}`); + removed++; + } + } + + return { removed }; +} + +/** + * Optimize workspace storage + */ +function optimizeStorage(workspacePath) { + const optimization = { + sessionsCleaned: 0, + directoriesCreated: 0, + logsArchived: false, + contextFilesCleaned: 0, + totalSpaceSaved: 0 + }; + + // Clean stale sessions + const sessionCleanup = cleanupStaleSessions(workspacePath); + optimization.sessionsCleaned = sessionCleanup.removed; + + // Repair directory structure + optimization.directoriesCreated = repairWorkspaceStructure(workspacePath); + + // Archive large logs + const logArchive = archiveLogs(workspacePath); + optimization.logsArchived = logArchive.archived; + if (logArchive.size) { + optimization.totalSpaceSaved += logArchive.size; + } + + // Clean empty context files + const contextCleanup = cleanupContextFiles(workspacePath); + optimization.contextFilesCleaned = contextCleanup.removed; + + return optimization; +} + +/** + * Validate workspace integrity + */ +function validateWorkspaceIntegrity(workspacePath) { + const issues = []; + const warnings = []; + + // Check workspace config + const configPath = path.join(workspacePath, 'workspace-config.json'); + if (!fs.existsSync(configPath)) { + issues.push('Missing workspace configuration file'); + } else { + try { + JSON.parse(fs.readFileSync(configPath, 'utf8')); + } catch (e) { + issues.push('Corrupted workspace configuration'); + } + } + + // Check directory permissions + try { + const testFile = path.join(workspacePath, '.write-test'); + fs.writeFileSync(testFile, 'test'); + fs.unlinkSync(testFile); + } catch (e) { + issues.push('Insufficient write permissions'); + } + + // Check session files integrity + const sessionsPath = path.join(workspacePath, 'sessions'); + if (fs.existsSync(sessionsPath)) { + const sessionFiles = fs.readdirSync(sessionsPath).filter(f => f.endsWith('.json')); + let corruptedSessions = 0; + + for (const file of sessionFiles) { + try { + JSON.parse(fs.readFileSync(path.join(sessionsPath, file), 'utf8')); + } catch (e) { + corruptedSessions++; + } + } + + if (corruptedSessions > 0) { + warnings.push(`${corruptedSessions} corrupted session files found`); + } + } + + return { issues, warnings }; +} + +/** + * Main cleanup function + */ +async function cleanupWorkspace(options = {}) { + try { + const workspacePath = path.join(process.cwd(), '.workspace'); + + if (!fs.existsSync(workspacePath)) { + console.error('โŒ Workspace directory not found.'); + console.error(' Run `npm run workspace-init` to initialize workspace'); + process.exit(1); + } + + console.log('๐Ÿงน BMAD Workspace Cleanup'); + console.log('========================'); + console.log(`๐Ÿ“ Workspace: ${workspacePath}`); + + // Validate integrity first + if (!options.skipValidation) { + console.log('\n๐Ÿ” Validating workspace integrity...'); + const validation = validateWorkspaceIntegrity(workspacePath); + + if (validation.issues.length > 0) { + console.log('โŒ Critical Issues Found:'); + validation.issues.forEach(issue => console.log(` โ€ข ${issue}`)); + } + + if (validation.warnings.length > 0) { + console.log('โš ๏ธ Warnings:'); + validation.warnings.forEach(warning => console.log(` โ€ข ${warning}`)); + } + + if (validation.issues.length === 0 && validation.warnings.length === 0) { + console.log('โœ… Workspace integrity OK'); + } + } + + // Perform optimization + console.log('\n๐Ÿ”ง Optimizing workspace...'); + const optimization = optimizeStorage(workspacePath); + + // Log cleanup activity + const logEntry = { + timestamp: new Date().toISOString(), + action: 'workspace-cleanup', + optimization: optimization, + user: process.env.USER || process.env.USERNAME || 'unknown' + }; + + const logPath = path.join(workspacePath, 'logs', 'workspace.log'); + fs.appendFileSync(logPath, JSON.stringify(logEntry) + '\n'); + + // Summary + console.log('\nโœ… Cleanup completed successfully'); + console.log('================================'); + console.log(`๐Ÿ—‘๏ธ Sessions cleaned: ${optimization.sessionsCleaned}`); + console.log(`๐Ÿ“ Directories created: ${optimization.directoriesCreated}`); + console.log(`๐Ÿ“ฆ Logs archived: ${optimization.logsArchived ? 'Yes' : 'No'}`); + console.log(`๐Ÿ—„๏ธ Context files cleaned: ${optimization.contextFilesCleaned}`); + + if (optimization.totalSpaceSaved > 0) { + console.log(`๐Ÿ’พ Space saved: ${optimization.totalSpaceSaved.toFixed(1)}MB`); + } + + console.log('\n๐Ÿš€ Workspace is now optimized and ready for use!'); + console.log(' Run `npm run workspace-status` to verify health'); + + } catch (error) { + console.error('โŒ Failed to cleanup workspace:', error.message); + process.exit(1); + } +} + +// Command line execution +if (require.main === module) { + const args = process.argv.slice(2); + const options = { + skipValidation: args.includes('--skip-validation'), + force: args.includes('--force') + }; + + cleanupWorkspace(options); +} + +module.exports = { cleanupWorkspace, validateWorkspaceIntegrity, optimizeStorage }; \ No newline at end of file diff --git a/workspace-utils/docs/cursor.md b/workspace-utils/docs/cursor.md new file mode 100644 index 00000000..a4a8418e --- /dev/null +++ b/workspace-utils/docs/cursor.md @@ -0,0 +1,215 @@ +# BMAD Workspace Integration - Cursor IDE + +## Overview +BMAD workspace utilities are fully compatible with Cursor IDE, providing seamless collaborative development experience across your team. + +## Setup + +### 1. Initialize Workspace +```bash +npm run workspace-init +``` +This will: +- Create `.workspace/` directory structure +- Set up session tracking for Cursor +- Generate Cursor-specific configuration files +- Create IDE-specific templates and examples + +### 2. Verify Installation +```bash +npm run workspace-status +``` +You should see: +- โœ… Active session detected (cursor) +- ๐Ÿ’š Health Score: 90+/100 +- ๐Ÿ“ All required directories present + +## Cursor-Specific Features + +### ๐ŸŽฏ Native Integration +- **Terminal Commands**: All workspace utilities available through Cursor's integrated terminal +- **Git Integration**: Workspace operations respect Cursor's git panel and version control +- **File Explorer**: Workspace directories appear in Cursor's file explorer with proper icons +- **Custom Rules**: Workspace state can be referenced in `.cursor/rules/` files + +### ๐Ÿ”ง Workspace Commands + +#### Session Management +```bash +npm run workspace-init # Start new collaboration session +npm run workspace-status # Check team activity and workspace health +npm run workspace-sync # Synchronize with latest team context +``` + +#### Agent Handoffs +```bash +npm run workspace-handoff # Interactive handoff creation +npm run workspace-handoff create --from dev --to qa --work "Feature implementation complete" +npm run workspace-handoff list # Show recent handoffs +npm run workspace-handoff agents # List available BMAD agents +``` + +#### Maintenance +```bash +npm run workspace-cleanup # Clean stale sessions and optimize storage +npm run workspace-health # Comprehensive health check and diagnostics +``` + +## Cursor Integration Patterns + +### 1. Custom Rules Integration +Create `.cursor/rules/workspace.md` to integrate workspace context: + +```markdown +# Workspace-Aware Development Rules + +## Context Integration +- Before making changes, check workspace context: `.workspace/context/sync-summary.md` +- Review recent handoffs: `.workspace/handoffs/` +- Check quality reports: `.workspace/quality/` + +## Agent Collaboration +- When ready for QA, use: `npm run workspace-handoff create --to qa` +- Before major changes, sync context: `npm run workspace-sync` +- Report issues in workspace context files for team visibility + +## Quality Standards +- Run workspace health check before commits: `npm run workspace-health` +- Maintain workspace cleanliness: `npm run workspace-cleanup` weekly +- Update workspace context with significant progress +``` + +### 2. Git Integration +The workspace system integrates with Cursor's git features: + +- **Pre-commit**: Workspace health automatically checked +- **Branch switching**: Session context preserved across branches +- **Merge conflicts**: Workspace context helps resolve conflicts +- **Commit messages**: Include workspace session ID for traceability + +### 3. File Organization +Cursor will show the workspace structure clearly: + +``` +๐Ÿ“ .workspace/ +โ”œโ”€โ”€ ๐Ÿ“‚ sessions/ # Current and past development sessions +โ”œโ”€โ”€ ๐Ÿ“‚ context/ # Shared development context +โ”œโ”€โ”€ ๐Ÿ“‚ handoffs/ # Agent-to-agent work transitions +โ”œโ”€โ”€ ๐Ÿ“‚ progress/ # Development progress tracking +โ”œโ”€โ”€ ๐Ÿ“‚ quality/ # Quality reports and metrics +โ”œโ”€โ”€ ๐Ÿ“‚ decisions/ # Architecture and design decisions +โ””โ”€โ”€ ๐Ÿ“‚ templates/ # IDE-specific setup guides +``` + +## Best Practices for Cursor Users + +### ๐Ÿš€ Starting Your Work Session +1. **Open terminal** in Cursor (Ctrl+` or Cmd+`) +2. **Initialize workspace**: `npm run workspace-init` +3. **Check team status**: `npm run workspace-status` +4. **Sync latest context**: `npm run workspace-sync` + +### ๐Ÿ”„ During Development +- **Update heartbeat**: Workspace automatically tracks your active session +- **Share context**: Add important findings to `.workspace/context/` +- **Track decisions**: Document choices in `.workspace/decisions/` +- **Monitor health**: Run `npm run workspace-health` if issues arise + +### ๐Ÿ“ค Handing Off Work +1. **Prepare handoff**: `npm run workspace-handoff create --to [agent]` +2. **Add context**: Include current work status and blockers +3. **Sync final state**: `npm run workspace-sync` +4. **Verify handoff**: Check `.workspace/handoffs/` for summary + +### ๐Ÿงน Weekly Maintenance +```bash +# Clean up workspace (run weekly) +npm run workspace-cleanup + +# Health check (run before important work) +npm run workspace-health + +# Full sync (run when switching contexts) +npm run workspace-sync +``` + +## Troubleshooting + +### Common Issues + +**"Workspace directory not found"** +- Solution: Run `npm run workspace-init` from your project root +- Verify you're in the correct project directory + +**"Permission denied" errors** +- Solution: Check file permissions on `.workspace/` directory +- Run `chmod -R 755 .workspace/` if needed (Unix/Mac) + +**"No active session found"** +- Solution: Initialize a new session with `npm run workspace-init` +- Or sync with existing sessions: `npm run workspace-sync` + +**Session conflicts** +- Multiple team members: Each gets unique session ID +- Session cleanup: Run `npm run workspace-cleanup` to remove stale sessions +- Health check: `npm run workspace-health` identifies session issues + +### IDE-Specific Issues + +**Cursor git panel not updating** +- Workspace operations are git-aware +- Refresh Cursor's git panel (Ctrl+Shift+P โ†’ "Git: Refresh") +- Check `.workspace/logs/workspace.log` for detailed activity + +**Terminal commands not found** +- Ensure you're in project root directory +- Verify `package.json` has workspace scripts +- Reinstall: `npm install` to refresh node_modules + +## Advanced Usage + +### Custom Workspace Scripts +Add project-specific workspace commands to `package.json`: + +```json +{ + "scripts": { + "workspace-deploy": "npm run workspace-sync && npm run deploy", + "workspace-test": "npm run workspace-health && npm test", + "workspace-reset": "npm run workspace-cleanup --force && npm run workspace-init" + } +} +``` + +### Environment Variables +Set Cursor-specific environment variables: + +```bash +# In your shell profile or .env file +export IDE_TYPE=cursor +export WORKSPACE_AUTO_SYNC=true +export WORKSPACE_LOG_LEVEL=info +``` + +### Integration with Cursor AI +When using Cursor's AI features, reference workspace context: + +1. **Ask AI to check**: "Review the workspace context in `.workspace/context/sync-summary.md`" +2. **Include handoff context**: "Consider the recent handoff in `.workspace/handoffs/`" +3. **Reference quality reports**: "Check quality metrics in `.workspace/quality/`" + +## Support + +### Getting Help +- **Workspace status**: `npm run workspace-status` shows current state +- **Health diagnostics**: `npm run workspace-health --verbose` for detailed analysis +- **Log files**: Check `.workspace/logs/workspace.log` for activity history + +### Team Coordination +- **Shared context**: All workspace data is git-trackable +- **Session visibility**: Team members can see active sessions +- **Handoff notifications**: Clear handoff documentation for smooth transitions + +--- + +*This guide is specific to Cursor IDE. For other IDEs, see the respective documentation in `workspace-utils/docs/`.* \ No newline at end of file diff --git a/workspace-utils/docs/github-copilot.md b/workspace-utils/docs/github-copilot.md new file mode 100644 index 00000000..df4b757c --- /dev/null +++ b/workspace-utils/docs/github-copilot.md @@ -0,0 +1,388 @@ +# BMAD Workspace Integration - GitHub Copilot + +## Overview +BMAD workspace utilities integrate seamlessly with GitHub Copilot in VS Code, providing enhanced AI-assisted development with intelligent context sharing and collaborative workspace management. + +## Setup + +### 1. Prerequisites +- VS Code with GitHub Copilot extension installed +- GitHub Copilot subscription active +- BMAD project initialized + +### 2. Initialize Workspace +```bash +npm run workspace-init +``` +This creates: +- Copilot-aware session tracking +- AI context files optimized for Copilot suggestions +- Integration with VS Code's workspace settings + +### 3. Verify Integration +```bash +npm run workspace-status +``` +Should show: +- โœ… Active session detected (github-copilot) +- ๐Ÿค– AI assistance integration enabled +- ๐Ÿ“ Copilot context files prepared + +## GitHub Copilot Integration Features + +### ๐Ÿค– AI-Enhanced Development +- **Context-Aware Suggestions**: Workspace context informs Copilot suggestions +- **Multi-File Understanding**: Copilot can reference workspace context across files +- **Collaborative AI**: Workspace tracks AI-assisted code for team visibility +- **Quality Integration**: AI suggestions tracked through workspace quality metrics + +### ๐Ÿง  Intelligent Context Management + +#### Copilot-Optimized Commands +```bash +npm run workspace-init # Creates Copilot-aware session +npm run workspace-status # Shows AI assistance metrics +npm run workspace-sync # Updates context for better Copilot suggestions +npm run workspace-health # Includes AI code quality checks +``` + +#### AI Context Files +```bash +# Context files optimized for Copilot +.workspace/context/copilot-context.md # Current development context +.workspace/context/code-patterns.md # Established code patterns +.workspace/context/ai-decisions.md # AI-assisted decisions +.workspace/quality/ai-metrics.json # AI code quality tracking +``` + +## VS Code + Copilot Integration Patterns + +### 1. Enhanced Code Completion +The workspace provides context that improves Copilot suggestions: + +```javascript +// File: .workspace/context/copilot-context.md +# Current Development Context + +## Project Patterns +- Using TypeScript with strict mode +- React functional components with hooks +- Jest for testing with custom matchers +- Error handling with custom error classes + +## Current Feature: User Authentication +- Implementing OAuth 2.0 flow +- Using JWT tokens for session management +- Validating with Zod schemas +- Following existing AuthService patterns +``` + +When Copilot sees this context, it generates more relevant suggestions that match your project patterns. + +### 2. AI-Assisted Agent Handoffs +```bash +# Handoff work with AI context +npm run workspace-handoff create --to dev --work "Copilot helped implement auth flow" --notes "AI suggested OAuth patterns, validated with team standards" + +# Track AI-assisted development +npm run workspace-sync # Updates AI metrics and context +``` + +### 3. Quality Tracking for AI Code +```bash +# Monitor AI-generated code quality +npm run workspace-health # Includes AI code metrics + +# Sample health report for AI code: +# - Copilot suggestion acceptance rate: 85% +# - AI-generated code coverage: 92% +# - Pattern consistency with existing code: 94% +``` + +## Best Practices for Copilot Users + +### ๐Ÿš€ Starting AI-Assisted Development + +#### 1. Initialize Context +```bash +# Start workspace +npm run workspace-init + +# Update context for Copilot +npm run workspace-sync +``` + +#### 2. Prepare Context Files +Create `.workspace/context/copilot-context.md`: +```markdown +# Development Context for Copilot + +## Current Sprint Goals +- Implement user authentication system +- Add data validation layer +- Create responsive dashboard UI + +## Code Standards +- TypeScript strict mode +- Functional React components +- Comprehensive error handling +- 90%+ test coverage requirement + +## Architecture Patterns +- Clean Architecture with dependency injection +- Repository pattern for data access +- Command/Query separation +- Event-driven updates +``` + +### ๐Ÿ”ง During Development + +#### Optimizing Copilot Suggestions +1. **Keep context updated**: Add relevant information to workspace context files +2. **Reference patterns**: Maintain `.workspace/context/code-patterns.md` with examples +3. **Track decisions**: Document AI-suggested approaches in `.workspace/decisions/` + +#### Context-Driven Development +```bash +# Before major feature work +npm run workspace-sync # Ensures Copilot has latest context + +# After Copilot generates significant code +npm run workspace-handoff create --notes "Copilot implemented OAuth flow" + +# Regular quality checks +npm run workspace-health # Monitor AI code quality +``` + +### ๐Ÿ“Š AI Code Quality Management + +#### Tracking AI Contributions +The workspace system tracks: +- **AI Suggestion Acceptance Rate**: How often you accept Copilot suggestions +- **Code Quality Metrics**: Quality of AI-generated vs human-written code +- **Pattern Consistency**: How well AI code matches project patterns +- **Test Coverage**: Coverage of AI-generated code vs requirements + +```bash +# View AI metrics +npm run workspace-health --ai-focus + +# Sample output: +# ๐Ÿค– AI Code Metrics: +# โ€ข Suggestion acceptance: 78% +# โ€ข Quality score: 92/100 +# โ€ข Pattern consistency: 89% +# โ€ข Test coverage: 85% +``` + +## VS Code Workspace Configuration + +### 1. Workspace Settings +Add to `.vscode/settings.json`: +```json +{ + "github.copilot.enable": { + "*": true, + "yaml": false, + "plaintext": false + }, + "github.copilot.advanced": { + "secret_key": "github-copilot-bmad-workspace", + "length": 500, + "temperature": 0.1 + }, + "bmad.workspace.integration": true, + "bmad.workspace.contextFiles": [ + ".workspace/context/copilot-context.md", + ".workspace/context/code-patterns.md" + ] +} +``` + +### 2. Tasks Integration +Add to `.vscode/tasks.json`: +```json +{ + "version": "2.0.0", + "tasks": [ + { + "label": "BMAD: Workspace Init", + "type": "shell", + "command": "npm run workspace-init", + "group": "build", + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared" + } + }, + { + "label": "BMAD: Sync Context for Copilot", + "type": "shell", + "command": "npm run workspace-sync", + "group": "build" + }, + { + "label": "BMAD: AI Health Check", + "type": "shell", + "command": "npm run workspace-health --ai-focus", + "group": "test", + "presentation": { + "reveal": "always" + } + } + ] +} +``` + +### 3. Keybindings +Add to `.vscode/keybindings.json`: +```json +[ + { + "key": "ctrl+shift+w ctrl+shift+i", + "command": "workbench.action.tasks.runTask", + "args": "BMAD: Workspace Init" + }, + { + "key": "ctrl+shift+w ctrl+shift+s", + "command": "workbench.action.tasks.runTask", + "args": "BMAD: Sync Context for Copilot" + }, + { + "key": "ctrl+shift+w ctrl+shift+h", + "command": "workbench.action.tasks.runTask", + "args": "BMAD: AI Health Check" + } +] +``` + +## Advanced Copilot Integration + +### 1. Context-Aware Prompts +Use workspace context to improve Copilot prompts: + +```javascript +// Example: Using workspace context in comments for better suggestions +// Based on .workspace/context/auth-patterns.md, implement OAuth login +// Following the AuthService pattern established in the workspace +// Include error handling as defined in .workspace/context/error-patterns.md +function loginWithOAuth(provider) { + // Copilot will generate code matching your established patterns +} +``` + +### 2. AI Decision Tracking +```bash +# Track AI-suggested architectural decisions +npm run workspace-handoff create --to architect --work "Review Copilot-suggested auth architecture" --notes "AI suggested JWT with refresh tokens, need validation" +``` + +### 3. Quality Gates for AI Code +```bash +# Set up quality gates that include AI metrics +npm run workspace-health --exit-code # Fails CI if AI code quality below threshold +``` + +## Troubleshooting + +### Common Issues + +**"Copilot not using workspace context"** +- Ensure context files are in `.workspace/context/` +- Update context with `npm run workspace-sync` +- Restart VS Code to refresh Copilot context + +**"Poor quality AI suggestions"** +- Check workspace context is current: `npm run workspace-status` +- Update code patterns: Edit `.workspace/context/code-patterns.md` +- Verify session tracking: Should show `github-copilot` IDE type + +**"AI metrics not tracking"** +- Initialize workspace: `npm run workspace-init` +- Check health status: `npm run workspace-health` +- Verify VS Code workspace settings include BMAD integration + +### Copilot-Specific Issues + +**"Suggestions don't match project patterns"** +- Update `.workspace/context/copilot-context.md` with current patterns +- Add examples to `.workspace/context/code-patterns.md` +- Sync context: `npm run workspace-sync` + +**"AI quality scores are low"** +- Review Copilot suggestion acceptance rate +- Update context files with better examples +- Consider adjusting Copilot temperature in VS Code settings + +## Performance Optimization + +### Context File Management +```bash +# Keep context files optimized for Copilot +npm run workspace-cleanup --ai-optimize + +# Compress large context files +npm run workspace-sync --compress +``` + +### Selective AI Context +Only include relevant context for current work: + +```markdown + +# Current Focus: Authentication Module + +## Relevant Patterns (for this sprint only) +- OAuth 2.0 implementation patterns +- JWT token validation +- Error handling for auth failures + +## Not Currently Relevant + + + +``` + +## Integration Examples + +### Example 1: Feature Development with AI Assistance +```bash +# 1. Initialize workspace +npm run workspace-init + +# 2. Prepare context for new feature +echo "Implementing user profile management with Copilot assistance" > .workspace/context/current-work.md + +# 3. Sync context for Copilot +npm run workspace-sync + +# 4. Develop with Copilot +# (VS Code: Copilot uses workspace context for better suggestions) + +# 5. Track AI contribution +npm run workspace-handoff create --work "Profile management with AI assistance" --notes "Copilot suggested efficient CRUD patterns" + +# 6. Quality check +npm run workspace-health --ai-focus +``` + +### Example 2: AI-Assisted Code Review +```bash +# 1. After development phase +npm run workspace-sync # Update context with recent changes + +# 2. Create review handoff +npm run workspace-handoff create --to qa --work "Review AI-assisted profile implementation" + +# 3. QA checks AI code quality +npm run workspace-health # Shows AI code metrics + +# 4. Address any quality issues +# (Use workspace context to improve code consistency) +``` + +--- + +*This guide optimizes GitHub Copilot integration with BMAD workspace for enhanced AI-assisted development. The workspace system provides context that improves AI suggestions while maintaining team collaboration and code quality.* \ No newline at end of file diff --git a/workspace-utils/docs/windsurf.md b/workspace-utils/docs/windsurf.md new file mode 100644 index 00000000..f3c90b74 --- /dev/null +++ b/workspace-utils/docs/windsurf.md @@ -0,0 +1,306 @@ +# BMAD Workspace Integration - Windsurf IDE + +## Overview +BMAD workspace utilities provide full compatibility with Windsurf IDE, enabling seamless AI-assisted collaborative development with intelligent workspace management. + +## Setup + +### 1. Initialize Workspace +```bash +npm run workspace-init +``` +Windsurf-specific setup includes: +- Session tracking optimized for Windsurf AI agent workflows +- Context sharing compatible with Windsurf's AI conversation memory +- Integration with Windsurf's project understanding capabilities + +### 2. Verify Integration +```bash +npm run workspace-status +``` +Expected output for Windsurf: +- โœ… Active session detected (windsurf) +- ๐Ÿค– AI agent compatibility enabled +- ๐Ÿ“ Workspace structure optimized for AI context + +## Windsurf-Specific Features + +### ๐Ÿค– AI Agent Integration +- **Context Continuity**: Workspace context integrates with Windsurf's AI memory +- **Agent Handoffs**: Seamless transitions between human developers and AI agents +- **Conversation Context**: Workspace state informs AI assistant conversations +- **Multi-Modal Support**: Workspace handles code, documentation, and AI interactions + +### ๐Ÿง  Intelligent Workspace Features + +#### AI-Enhanced Session Management +```bash +npm run workspace-init # Creates AI-aware session with conversation context +npm run workspace-status # Shows AI agent activity and human collaboration +npm run workspace-sync # Synchronizes with Windsurf AI conversation memory +``` + +#### Smart Agent Handoffs +```bash +# Handoff to AI agent +npm run workspace-handoff create --to ai --work "Implement user authentication" + +# Handoff to human developer +npm run workspace-handoff create --to dev --work "Review AI-generated code" + +# Handoff to QA with AI context +npm run workspace-handoff create --to qa --work "Test AI-implemented features" +``` + +## Windsurf Integration Patterns + +### 1. AI Conversation Context +The workspace system automatically integrates with Windsurf's AI conversations: + +```markdown +# Example: AI Context Integration +## Current Workspace State +- **Session**: windsurf-session-abc123 +- **Active Work**: Feature development with AI assistance +- **Context Files**: 12 shared context files +- **Recent Handoffs**: AI โ†’ Developer โ†’ QA + +## AI Conversation Summary +- Discussed authentication implementation approach +- Generated user model and service layer +- Identified testing requirements for AI-generated code +``` + +### 2. Multi-Agent Workflows +Windsurf supports both AI and human agents in the same workspace: + +```bash +# Check active agents (AI + human) +npm run workspace-status + +# Create handoff between AI and human agents +npm run workspace-handoff create --from ai --to dev --work "Code review needed" + +# Sync context for AI understanding +npm run workspace-sync +``` + +### 3. Intelligent Context Sharing +The workspace adapts to Windsurf's AI capabilities: + +- **Code Understanding**: AI agents can reference workspace context files +- **Conversation Memory**: Workspace state informs ongoing AI conversations +- **Decision Tracking**: AI and human decisions are recorded together +- **Quality Monitoring**: AI-generated code tracked through quality metrics + +## Best Practices for Windsurf Users + +### ๐Ÿš€ Starting AI-Assisted Development +1. **Initialize workspace**: `npm run workspace-init` +2. **Verify AI integration**: Check session shows `windsurf` IDE type +3. **Sync with AI context**: `npm run workspace-sync` +4. **Begin AI conversation**: Reference workspace context in Windsurf chat + +### ๐Ÿค– Working with AI Agents +- **Context Sharing**: Add important context to `.workspace/context/` for AI reference +- **Decision Recording**: Document AI suggestions in `.workspace/decisions/` +- **Quality Tracking**: Monitor AI-generated code through workspace quality reports +- **Handoff Preparation**: Use workspace handoffs when switching between AI and human work + +### ๐Ÿ”„ AI-Human Collaboration Patterns + +#### Pattern 1: AI-First Development +```bash +# 1. Start with AI agent +npm run workspace-handoff create --to ai --work "Initial implementation" + +# 2. AI implements core functionality +# (AI adds context to workspace automatically) + +# 3. Handoff to human for review +npm run workspace-handoff create --from ai --to dev --work "Review and refine" + +# 4. Human reviews and improves +npm run workspace-sync # Get latest AI context + +# 5. Handoff to QA +npm run workspace-handoff create --to qa --work "Test AI-assisted implementation" +``` + +#### Pattern 2: Human-AI Pair Programming +```bash +# Continuous sync during pair programming +npm run workspace-sync # Before AI conversation +# ... work with AI in Windsurf ... +npm run workspace-sync # After AI generates code +``` + +### ๐Ÿ“Š Quality Monitoring for AI Code +```bash +# Health check includes AI code quality metrics +npm run workspace-health + +# Specific checks for AI-generated code: +# - Code consistency with human patterns +# - Integration with existing codebase +# - Test coverage for AI implementations +``` + +## Windsurf-Specific Configuration + +### Environment Variables +```bash +# Set in your environment or .env file +export IDE_TYPE=windsurf +export WINDSURF_AI_INTEGRATION=true +export WORKSPACE_AI_CONTEXT=true +``` + +### AI Context Files +The workspace creates Windsurf-specific context files: + +``` +๐Ÿ“ .workspace/ +โ”œโ”€โ”€ ๐Ÿ“‚ ai-context/ # AI conversation summaries +โ”œโ”€โ”€ ๐Ÿ“‚ ai-decisions/ # AI-suggested architectural decisions +โ”œโ”€โ”€ ๐Ÿ“‚ ai-handoffs/ # AI โ†” Human work transitions +โ””โ”€โ”€ ๐Ÿ“‚ ai-quality/ # Quality metrics for AI-generated code +``` + +### Windsurf AI Prompts +Use these patterns in Windsurf AI conversations: + +``` +"Check the workspace context in .workspace/context/sync-summary.md before implementing" + +"Consider the recent handoff details in .workspace/handoffs/ for this feature" + +"Review the workspace quality metrics in .workspace/quality/ to ensure consistency" + +"Update the workspace context with your implementation approach" +``` + +## Advanced Windsurf Integration + +### 1. Custom AI Workflows +```json +// package.json additions for AI workflows +{ + "scripts": { + "ai-handoff": "npm run workspace-handoff create --to ai", + "ai-review": "npm run workspace-sync && echo 'Context ready for AI review'", + "ai-quality": "npm run workspace-health --ai-focus" + } +} +``` + +### 2. AI Context Optimization +```bash +# Optimize workspace for AI understanding +npm run workspace-cleanup --ai-optimize + +# Generate AI-friendly summaries +npm run workspace-sync --ai-summary + +# Health check with AI code focus +npm run workspace-health --ai-metrics +``` + +### 3. Multi-Modal Context +Windsurf can handle various content types in workspace: + +- **Code Files**: Traditional source code with AI annotations +- **Documentation**: AI-generated and human-written docs +- **Conversations**: AI chat history integrated with workspace +- **Decisions**: Joint AI-human architectural decisions +- **Quality Reports**: AI code quality metrics and human reviews + +## Troubleshooting + +### AI Integration Issues + +**"AI context not loading"** +- Verify `WINDSURF_AI_INTEGRATION=true` environment variable +- Check workspace initialization: `npm run workspace-init` +- Sync context manually: `npm run workspace-sync` + +**"Handoff between AI and human not working"** +- Ensure both agent types are recognized: `npm run workspace-handoff agents` +- Check session is properly initialized for Windsurf +- Verify workspace structure: `npm run workspace-health` + +**"AI not referencing workspace context"** +- Explicitly reference context files in AI conversations +- Use workspace sync before AI conversations: `npm run workspace-sync` +- Check context file permissions and content + +### Windsurf-Specific Issues + +**"Windsurf not detecting workspace"** +- Initialize from project root: `npm run workspace-init` +- Check IDE detection: Session should show `windsurf` type +- Restart Windsurf if needed + +**"AI conversation memory conflicts with workspace"** +- Workspace context complements AI memory, not replaces it +- Use `npm run workspace-sync` to align contexts +- Clear workspace if needed: `npm run workspace-cleanup --force` + +## Performance Optimization + +### AI Context Efficiency +- **Selective Context**: Only share relevant context with AI +- **Context Summarization**: Use workspace summaries for large projects +- **Regular Cleanup**: Remove outdated AI context regularly + +```bash +# Optimize workspace for AI performance +npm run workspace-cleanup --ai-optimize + +# Generate efficient AI summaries +npm run workspace-sync --compress +``` + +### Memory Management +- **Conversation Limits**: Workspace helps track long AI conversations +- **Context Rotation**: Older context automatically archived +- **Session Cleanup**: Stale AI sessions cleaned up automatically + +## Integration Examples + +### Example 1: AI Feature Implementation +```bash +# 1. Initialize workspace for AI work +npm run workspace-init + +# 2. Create handoff to AI +npm run workspace-handoff create --to ai --work "Implement user dashboard" + +# 3. Work with AI in Windsurf +# AI: "I see from the workspace context that we're using React. I'll implement..." + +# 4. AI completes work, human reviews +npm run workspace-sync # Get AI's context updates + +# 5. Handoff to QA +npm run workspace-handoff create --from ai --to qa --work "Test dashboard implementation" +``` + +### Example 2: AI Code Review +```bash +# 1. Human completes feature +npm run workspace-handoff create --to ai --work "Review authentication logic" + +# 2. AI reviews with workspace context +# AI: "Based on the workspace quality metrics, I recommend..." + +# 3. Apply AI suggestions +npm run workspace-sync # Update with AI feedback + +# 4. Final quality check +npm run workspace-health --ai-review +``` + +--- + +*This guide is optimized for Windsurf IDE's AI capabilities. The workspace system enhances AI-human collaboration while maintaining compatibility with traditional development workflows.* \ No newline at end of file diff --git a/workspace-utils/handoff.js b/workspace-utils/handoff.js new file mode 100644 index 00000000..59923325 --- /dev/null +++ b/workspace-utils/handoff.js @@ -0,0 +1,399 @@ +#!/usr/bin/env node +/** + * BMAD Workspace Handoff Utility + * Cross-IDE agent handoff management and coordination + */ + +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +/** + * Get available BMAD agents + */ +function getAvailableAgents() { + const agents = [ + { id: 'dev', name: 'Developer (James)', description: 'Code implementation and debugging' }, + { id: 'qa', name: 'QA Engineer (Quinn)', description: 'Quality validation and testing' }, + { id: 'sm', name: 'Scrum Master (Morgan)', description: 'Story creation and project coordination' }, + { id: 'analyst', name: 'Business Analyst (Alex)', description: 'Requirements analysis and research' }, + { id: 'architect', name: 'Technical Architect (Sam)', description: 'System design and architecture' }, + { id: 'ux-expert', name: 'UX Expert (Jordan)', description: 'User experience and interface design' }, + { id: 'pm', name: 'Product Manager (John)', description: 'Product strategy and PRD creation' }, + { id: 'po', name: 'Product Owner (Sarah)', description: 'Backlog management and acceptance criteria' } + ]; + + return agents; +} + +/** + * Create handoff context package + */ +function createHandoffContext(workspacePath, fromAgent, toAgent, currentWork, notes = '') { + const handoffId = crypto.randomBytes(6).toString('hex'); + const timestamp = new Date().toISOString(); + + // Gather current workspace context + const contextPath = path.join(workspacePath, 'context'); + const contextFiles = fs.existsSync(contextPath) ? + fs.readdirSync(contextPath).filter(f => f.endsWith('.md') || f.endsWith('.json')) : []; + + // Get recent progress + const progressPath = path.join(workspacePath, 'progress'); + const recentProgress = []; + if (fs.existsSync(progressPath)) { + const progressFiles = fs.readdirSync(progressPath) + .filter(f => f.endsWith('.md')) + .sort() + .slice(-5); // Last 5 progress files + + for (const file of progressFiles) { + try { + const content = fs.readFileSync(path.join(progressPath, file), 'utf8'); + recentProgress.push({ + file: file, + preview: content.substring(0, 200) + (content.length > 200 ? '...' : '') + }); + } catch (e) { + // Skip corrupted files + } + } + } + + // Get current session info + const sessionsPath = path.join(workspacePath, 'sessions'); + let currentSession = null; + if (fs.existsSync(sessionsPath)) { + const sessionFiles = fs.readdirSync(sessionsPath).filter(f => f.endsWith('.json')); + for (const file of sessionFiles) { + try { + const sessionData = JSON.parse(fs.readFileSync(path.join(sessionsPath, file), 'utf8')); + const lastHeartbeat = new Date(sessionData.lastHeartbeat); + const timeDiff = new Date() - lastHeartbeat; + if (timeDiff < 3600000) { // Active within last hour + currentSession = sessionData; + break; + } + } catch (e) { + // Skip corrupted session files + } + } + } + + const handoffData = { + id: handoffId, + timestamp: timestamp, + fromAgent: fromAgent, + toAgent: toAgent, + currentWork: currentWork, + notes: notes, + session: currentSession, + context: { + availableFiles: contextFiles, + recentProgress: recentProgress, + workspaceHealth: checkBasicHealth(workspacePath) + }, + recommendations: generateHandoffRecommendations(fromAgent, toAgent, currentWork), + status: 'pending' + }; + + return handoffData; +} + +/** + * Check basic workspace health + */ +function checkBasicHealth(workspacePath) { + const requiredDirs = ['sessions', 'context', 'handoffs', 'progress']; + const missingDirs = []; + + for (const dir of requiredDirs) { + if (!fs.existsSync(path.join(workspacePath, dir))) { + missingDirs.push(dir); + } + } + + return { + score: missingDirs.length === 0 ? 100 : Math.max(0, 100 - (missingDirs.length * 25)), + missingDirectories: missingDirs + }; +} + +/** + * Generate handoff recommendations + */ +function generateHandoffRecommendations(fromAgent, toAgent, currentWork) { + const recommendations = []; + + // Agent-specific recommendations + if (fromAgent === 'dev' && toAgent === 'qa') { + recommendations.push('Ensure all code changes are committed and pushed'); + recommendations.push('Run tests and provide test results'); + recommendations.push('Document any known issues or edge cases'); + recommendations.push('Specify testing priorities and focus areas'); + } else if (fromAgent === 'sm' && toAgent === 'dev') { + recommendations.push('Review story acceptance criteria carefully'); + recommendations.push('Clarify any ambiguous requirements'); + recommendations.push('Confirm technical approach with architect if needed'); + recommendations.push('Set up development environment if not ready'); + } else if (fromAgent === 'analyst' && toAgent === 'pm') { + recommendations.push('Summarize key research findings'); + recommendations.push('Highlight market opportunities and constraints'); + recommendations.push('Provide user persona insights'); + recommendations.push('Recommend feature prioritization approach'); + } else if (fromAgent === 'architect' && toAgent === 'dev') { + recommendations.push('Review architectural decisions and constraints'); + recommendations.push('Ensure development setup matches architecture'); + recommendations.push('Clarify any technical implementation details'); + recommendations.push('Verify third-party dependencies are available'); + } else if (['dev', 'qa'].includes(fromAgent) && toAgent === 'sm') { + recommendations.push('Provide status update on current story'); + recommendations.push('Report any blockers or impediments'); + recommendations.push('Suggest story scope adjustments if needed'); + recommendations.push('Update story progress and completion estimates'); + } + + // Work-specific recommendations + const workLower = currentWork.toLowerCase(); + if (workLower.includes('bug') || workLower.includes('fix')) { + recommendations.push('Provide detailed bug reproduction steps'); + recommendations.push('Include error logs and stack traces'); + recommendations.push('Identify root cause if known'); + } else if (workLower.includes('feature') || workLower.includes('story')) { + recommendations.push('Confirm feature requirements are clear'); + recommendations.push('Verify acceptance criteria are testable'); + recommendations.push('Ensure dependencies are identified'); + } else if (workLower.includes('refactor')) { + recommendations.push('Document current implementation patterns'); + recommendations.push('Explain refactoring goals and benefits'); + recommendations.push('Identify areas of highest risk'); + } + + // General recommendations + recommendations.push('Update workspace context with latest findings'); + recommendations.push('Review any TODO items or pending decisions'); + + return recommendations; +} + +/** + * Save handoff to workspace + */ +function saveHandoff(workspacePath, handoffData) { + const handoffsPath = path.join(workspacePath, 'handoffs'); + if (!fs.existsSync(handoffsPath)) { + fs.mkdirSync(handoffsPath, { recursive: true }); + } + + const handoffFile = path.join(handoffsPath, `${handoffData.id}.json`); + fs.writeFileSync(handoffFile, JSON.stringify(handoffData, null, 2)); + + // Also create a markdown summary for easy reading + const markdownFile = path.join(handoffsPath, `${handoffData.id}.md`); + const markdownContent = generateHandoffMarkdown(handoffData); + fs.writeFileSync(markdownFile, markdownContent); + + return { handoffFile, markdownFile }; +} + +/** + * Generate handoff markdown summary + */ +function generateHandoffMarkdown(handoffData) { + const toAgentInfo = getAvailableAgents().find(a => a.id === handoffData.toAgent); + + return `# Agent Handoff: ${handoffData.fromAgent} โ†’ ${handoffData.toAgent} + +**Handoff ID:** ${handoffData.id} +**Timestamp:** ${new Date(handoffData.timestamp).toLocaleString()} +**To Agent:** ${toAgentInfo?.name || handoffData.toAgent} - ${toAgentInfo?.description || 'Unknown agent'} + +## Current Work +${handoffData.currentWork} + +## Notes +${handoffData.notes || 'No additional notes provided'} + +## Context Summary +- **Available context files:** ${handoffData.context.availableFiles.length} +- **Recent progress entries:** ${handoffData.context.recentProgress.length} +- **Workspace health:** ${handoffData.context.workspaceHealth.score}/100 + +${handoffData.context.recentProgress.length > 0 ? ` +## Recent Progress +${handoffData.context.recentProgress.map((p, i) => ` +### ${i + 1}. ${p.file} +${p.preview} +`).join('')} +` : ''} + +## Recommendations +${handoffData.recommendations.map(r => `- ${r}`).join('\n')} + +## Session Information +${handoffData.session ? ` +- **IDE:** ${handoffData.session.ide} +- **User:** ${handoffData.session.user} +- **Created:** ${new Date(handoffData.session.created).toLocaleString()} +- **Last Activity:** ${new Date(handoffData.session.lastHeartbeat).toLocaleString()} +` : 'No active session found'} + +--- +*Generated by BMAD Cross-IDE Workspace System* +`; +} + +/** + * List recent handoffs + */ +function listRecentHandoffs(workspacePath, limit = 10) { + const handoffsPath = path.join(workspacePath, 'handoffs'); + if (!fs.existsSync(handoffsPath)) { + return []; + } + + const handoffFiles = fs.readdirSync(handoffsPath) + .filter(f => f.endsWith('.json')) + .map(f => { + try { + const content = fs.readFileSync(path.join(handoffsPath, f), 'utf8'); + return JSON.parse(content); + } catch (e) { + return null; + } + }) + .filter(Boolean) + .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)) + .slice(0, limit); + + return handoffFiles; +} + +/** + * Main handoff function + */ +async function manageHandoff(action = 'create', options = {}) { + try { + const workspacePath = path.join(process.cwd(), '.workspace'); + + if (!fs.existsSync(workspacePath)) { + console.error('โŒ Workspace directory not found.'); + console.error(' Run `npm run workspace-init` to initialize workspace'); + process.exit(1); + } + + if (action === 'list') { + console.log('๐Ÿ”„ Recent Agent Handoffs'); + console.log('========================'); + + const handoffs = listRecentHandoffs(workspacePath); + if (handoffs.length === 0) { + console.log('No handoffs found.'); + return; + } + + handoffs.forEach((handoff, index) => { + const toAgentInfo = getAvailableAgents().find(a => a.id === handoff.toAgent); + console.log(`${index + 1}. ${handoff.id} - ${handoff.fromAgent} โ†’ ${handoff.toAgent}`); + console.log(` ${toAgentInfo?.name || handoff.toAgent}`); + console.log(` ${new Date(handoff.timestamp).toLocaleString()}`); + console.log(` Work: ${handoff.currentWork.substring(0, 80)}${handoff.currentWork.length > 80 ? '...' : ''}`); + console.log(''); + }); + + return; + } + + if (action === 'agents') { + console.log('๐Ÿ‘ฅ Available BMAD Agents'); + console.log('========================'); + + const agents = getAvailableAgents(); + agents.forEach((agent, index) => { + console.log(`${index + 1}. ${agent.id} - ${agent.name}`); + console.log(` ${agent.description}`); + console.log(''); + }); + + return; + } + + // Default create action + const fromAgent = options.from || 'unknown'; + const toAgent = options.to || 'dev'; + const currentWork = options.work || 'No work description provided'; + const notes = options.notes || ''; + + console.log('๐Ÿ”„ Creating Agent Handoff'); + console.log('========================='); + + // Validate agents + const agents = getAvailableAgents(); + const toAgentInfo = agents.find(a => a.id === toAgent); + + if (!toAgentInfo) { + console.error(`โŒ Unknown target agent: ${toAgent}`); + console.error('Available agents:'); + agents.forEach(a => console.error(` ${a.id} - ${a.name}`)); + process.exit(1); + } + + // Create handoff context + const handoffData = createHandoffContext(workspacePath, fromAgent, toAgent, currentWork, notes); + + // Save handoff + const files = saveHandoff(workspacePath, handoffData); + + // Log handoff activity + const logEntry = { + timestamp: new Date().toISOString(), + action: 'agent-handoff', + handoffId: handoffData.id, + fromAgent: fromAgent, + toAgent: toAgent, + user: process.env.USER || process.env.USERNAME || 'unknown' + }; + + const logPath = path.join(workspacePath, 'logs', 'workspace.log'); + fs.appendFileSync(logPath, JSON.stringify(logEntry) + '\n'); + + // Success output + console.log('โœ… Handoff created successfully'); + console.log('=============================='); + console.log(`๐Ÿ†” Handoff ID: ${handoffData.id}`); + console.log(`๐Ÿ‘ค From: ${fromAgent} โ†’ ${toAgentInfo.name}`); + console.log(`๐Ÿ“ Work: ${currentWork}`); + console.log(`๐Ÿ“ Handoff file: ${path.basename(files.handoffFile)}`); + console.log(`๐Ÿ“„ Summary: ${path.basename(files.markdownFile)}`); + console.log(`\n๐Ÿ“‹ Recommendations for ${toAgentInfo.name}:`); + handoffData.recommendations.forEach(rec => console.log(` โ€ข ${rec}`)); + + console.log('\n๐Ÿš€ Next steps:'); + console.log(` 1. Review handoff details in: .workspace/handoffs/${handoffData.id}.md`); + console.log(` 2. Start working with the ${toAgentInfo.name} agent`); + console.log(` 3. Update workspace context as work progresses`); + + } catch (error) { + console.error('โŒ Failed to manage handoff:', error.message); + process.exit(1); + } +} + +// Command line execution +if (require.main === module) { + const args = process.argv.slice(2); + const action = args[0] || 'create'; + + const options = {}; + for (let i = 1; i < args.length; i += 2) { + const key = args[i]?.replace('--', ''); + const value = args[i + 1]; + if (key && value) { + options[key] = value; + } + } + + manageHandoff(action, options); +} + +module.exports = { manageHandoff, createHandoffContext, getAvailableAgents }; \ No newline at end of file diff --git a/workspace-utils/health.js b/workspace-utils/health.js new file mode 100644 index 00000000..aa240bd6 --- /dev/null +++ b/workspace-utils/health.js @@ -0,0 +1,549 @@ +#!/usr/bin/env node +/** + * BMAD Workspace Health Check Utility + * Cross-IDE workspace health monitoring and diagnostics + */ + +const fs = require('fs'); +const path = require('path'); + +/** + * Check directory structure integrity + */ +function checkDirectoryStructure(workspacePath) { + const requiredDirs = [ + { name: 'sessions', critical: true, description: 'Session management' }, + { name: 'context', critical: true, description: 'Shared context storage' }, + { name: 'handoffs', critical: true, description: 'Agent handoff coordination' }, + { name: 'decisions', critical: false, description: 'Decision tracking' }, + { name: 'progress', critical: false, description: 'Progress monitoring' }, + { name: 'quality', critical: false, description: 'Quality reports' }, + { name: 'archive', critical: false, description: 'Archived data' }, + { name: 'hooks', critical: false, description: 'Integration hooks' }, + { name: 'templates', critical: false, description: 'Workspace templates' }, + { name: 'logs', critical: true, description: 'Activity logging' } + ]; + + const results = { + score: 100, + issues: [], + missing: [], + present: [] + }; + + for (const dir of requiredDirs) { + const dirPath = path.join(workspacePath, dir.name); + if (fs.existsSync(dirPath)) { + results.present.push(dir); + } else { + results.missing.push(dir); + const penalty = dir.critical ? 15 : 5; + results.score -= penalty; + results.issues.push(`Missing ${dir.critical ? 'critical' : 'optional'} directory: ${dir.name} (${dir.description})`); + } + } + + return results; +} + +/** + * Check workspace configuration + */ +function checkWorkspaceConfig(workspacePath) { + const configPath = path.join(workspacePath, 'workspace-config.json'); + const results = { + score: 100, + issues: [], + valid: false, + config: null + }; + + if (!fs.existsSync(configPath)) { + results.score = 0; + results.issues.push('Missing workspace configuration file'); + return results; + } + + try { + const configContent = fs.readFileSync(configPath, 'utf8'); + const config = JSON.parse(configContent); + + // Validate required fields + const requiredFields = ['version', 'created', 'features', 'settings']; + for (const field of requiredFields) { + if (!config[field]) { + results.score -= 20; + results.issues.push(`Missing required config field: ${field}`); + } + } + + // Check feature flags + if (config.features) { + const expectedFeatures = ['crossIDESupport', 'sessionManagement', 'contextPersistence', 'agentHandoffs']; + for (const feature of expectedFeatures) { + if (config.features[feature] !== true) { + results.score -= 5; + results.issues.push(`Feature not enabled: ${feature}`); + } + } + } + + results.valid = true; + results.config = config; + + } catch (e) { + results.score = 0; + results.issues.push(`Corrupted configuration file: ${e.message}`); + } + + return results; +} + +/** + * Check session health + */ +function checkSessionHealth(workspacePath) { + const sessionsPath = path.join(workspacePath, 'sessions'); + const results = { + score: 100, + issues: [], + totalSessions: 0, + activeSessions: 0, + staleSessions: 0, + corruptedSessions: 0, + sessions: [] + }; + + if (!fs.existsSync(sessionsPath)) { + results.score = 0; + results.issues.push('Sessions directory not found'); + return results; + } + + const sessionFiles = fs.readdirSync(sessionsPath).filter(f => f.endsWith('.json')); + results.totalSessions = sessionFiles.length; + + const now = new Date(); + + for (const file of sessionFiles) { + try { + const sessionPath = path.join(sessionsPath, file); + const sessionContent = fs.readFileSync(sessionPath, 'utf8'); + const sessionData = JSON.parse(sessionContent); + + // Validate session structure + const requiredFields = ['id', 'created', 'lastHeartbeat', 'ide', 'user']; + let isValid = true; + + for (const field of requiredFields) { + if (!sessionData[field]) { + isValid = false; + break; + } + } + + if (!isValid) { + results.corruptedSessions++; + results.score -= 5; + results.issues.push(`Invalid session structure: ${file}`); + continue; + } + + // Check session freshness + const lastHeartbeat = new Date(sessionData.lastHeartbeat); + const timeDiff = now - lastHeartbeat; + + if (timeDiff < 3600000) { // 1 hour + results.activeSessions++; + sessionData.status = 'active'; + } else if (timeDiff < 86400000) { // 24 hours + sessionData.status = 'idle'; + } else { + results.staleSessions++; + sessionData.status = 'stale'; + } + + sessionData.timeSinceLastHeartbeat = timeDiff; + results.sessions.push(sessionData); + + } catch (e) { + results.corruptedSessions++; + results.score -= 10; + results.issues.push(`Corrupted session file: ${file}`); + } + } + + // Penalty for too many stale sessions + if (results.staleSessions > 5) { + results.score -= (results.staleSessions - 5) * 2; + results.issues.push(`Excessive stale sessions: ${results.staleSessions}`); + } + + return results; +} + +/** + * Check file system permissions + */ +function checkFileSystemPermissions(workspacePath) { + const results = { + score: 100, + issues: [], + canRead: false, + canWrite: false, + canExecute: false + }; + + try { + // Test read permission + fs.readdirSync(workspacePath); + results.canRead = true; + + // Test write permission + const testFile = path.join(workspacePath, '.health-check-write-test'); + fs.writeFileSync(testFile, 'test'); + fs.unlinkSync(testFile); + results.canWrite = true; + + // Test execute permission (create and run a temporary script) + const testScript = path.join(workspacePath, '.health-check-exec-test.js'); + fs.writeFileSync(testScript, 'console.log("test");'); + + // Try to require the file to test execution capability + require(testScript); + fs.unlinkSync(testScript); + results.canExecute = true; + + } catch (e) { + if (!results.canRead) { + results.score = 0; + results.issues.push('Cannot read workspace directory'); + } else if (!results.canWrite) { + results.score -= 50; + results.issues.push('Cannot write to workspace directory'); + } else if (!results.canExecute) { + results.score -= 20; + results.issues.push('Limited script execution permissions'); + } + } + + return results; +} + +/** + * Check log file health + */ +function checkLogHealth(workspacePath) { + const logPath = path.join(workspacePath, 'logs', 'workspace.log'); + const results = { + score: 100, + issues: [], + exists: false, + size: 0, + recentEntries: 0, + corruptedEntries: 0 + }; + + if (!fs.existsSync(logPath)) { + results.score -= 30; + results.issues.push('Workspace log file not found'); + return results; + } + + try { + const stats = fs.statSync(logPath); + results.exists = true; + results.size = stats.size; + + // Check log size + const logSizeMB = stats.size / (1024 * 1024); + if (logSizeMB > 50) { + results.score -= 15; + results.issues.push(`Large log file: ${logSizeMB.toFixed(1)}MB`); + } + + // Analyze recent log entries + const logContent = fs.readFileSync(logPath, 'utf8'); + const logLines = logContent.trim().split('\n'); + + const now = new Date(); + const oneDayAgo = now - 86400000; // 24 hours + + for (const line of logLines.slice(-100)) { // Check last 100 entries + if (line.trim() === '') continue; + + try { + const entry = JSON.parse(line); + const entryTime = new Date(entry.timestamp); + + if (entryTime > oneDayAgo) { + results.recentEntries++; + } + } catch (e) { + results.corruptedEntries++; + } + } + + if (results.corruptedEntries > 10) { + results.score -= results.corruptedEntries; + results.issues.push(`Multiple corrupted log entries: ${results.corruptedEntries}`); + } + + if (results.recentEntries === 0) { + results.score -= 20; + results.issues.push('No recent activity in logs'); + } + + } catch (e) { + results.score -= 25; + results.issues.push(`Cannot analyze log file: ${e.message}`); + } + + return results; +} + +/** + * Check cross-IDE compatibility features + */ +function checkCrossIDECompatibility(workspacePath) { + const results = { + score: 100, + issues: [], + ideSupport: {}, + templateCount: 0, + hookCount: 0 + }; + + // Check for IDE-specific templates + const templatesPath = path.join(workspacePath, 'templates'); + if (fs.existsSync(templatesPath)) { + const templateFiles = fs.readdirSync(templatesPath).filter(f => f.endsWith('.md')); + results.templateCount = templateFiles.length; + + const supportedIDEs = ['cursor', 'windsurf', 'vscode', 'trae', 'roo', 'cline', 'gemini', 'github-copilot']; + + for (const ide of supportedIDEs) { + const ideTemplate = templateFiles.find(f => f.includes(ide)); + results.ideSupport[ide] = !!ideTemplate; + + if (!ideTemplate) { + results.score -= 5; + } + } + + if (results.templateCount < 4) { + results.issues.push(`Limited IDE template support: ${results.templateCount} templates found`); + } + } else { + results.score -= 30; + results.issues.push('IDE templates directory not found'); + } + + // Check for integration hooks + const hooksPath = path.join(workspacePath, 'hooks'); + if (fs.existsSync(hooksPath)) { + const hookFiles = fs.readdirSync(hooksPath); + results.hookCount = hookFiles.length; + + if (results.hookCount === 0) { + results.score -= 10; + results.issues.push('No integration hooks configured'); + } + } + + return results; +} + +/** + * Generate comprehensive health report + */ +function generateHealthReport(workspacePath) { + const report = { + timestamp: new Date().toISOString(), + overallScore: 0, + status: 'unknown', + checks: { + directoryStructure: checkDirectoryStructure(workspacePath), + workspaceConfig: checkWorkspaceConfig(workspacePath), + sessionHealth: checkSessionHealth(workspacePath), + fileSystemPermissions: checkFileSystemPermissions(workspacePath), + logHealth: checkLogHealth(workspacePath), + crossIDECompatibility: checkCrossIDECompatibility(workspacePath) + }, + summary: { + totalIssues: 0, + criticalIssues: 0, + recommendations: [] + } + }; + + // Calculate overall score and issues + const checks = Object.values(report.checks); + const totalScore = checks.reduce((sum, check) => sum + check.score, 0); + report.overallScore = Math.round(totalScore / checks.length); + + // Collect all issues + const allIssues = checks.flatMap(check => check.issues || []); + report.summary.totalIssues = allIssues.length; + report.summary.criticalIssues = allIssues.filter(issue => + issue.includes('Missing critical') || + issue.includes('Cannot') || + issue.includes('Corrupted') + ).length; + + // Determine status + if (report.overallScore >= 90) { + report.status = 'excellent'; + } else if (report.overallScore >= 80) { + report.status = 'good'; + } else if (report.overallScore >= 70) { + report.status = 'fair'; + } else if (report.overallScore >= 60) { + report.status = 'poor'; + } else { + report.status = 'critical'; + } + + // Generate recommendations + if (report.checks.directoryStructure.missing.length > 0) { + report.summary.recommendations.push('Run `npm run workspace-cleanup` to repair directory structure'); + } + + if (report.checks.sessionHealth.staleSessions > 5) { + report.summary.recommendations.push('Clean up stale sessions with `npm run workspace-cleanup`'); + } + + if (report.checks.logHealth.size > 52428800) { // 50MB + report.summary.recommendations.push('Archive large log files to improve performance'); + } + + if (report.checks.crossIDECompatibility.templateCount < 4) { + report.summary.recommendations.push('Generate additional IDE-specific templates for better compatibility'); + } + + if (report.summary.criticalIssues > 0) { + report.summary.recommendations.push('Address critical issues immediately before continuing development'); + } + + return report; +} + +/** + * Display health report + */ +function displayHealthReport(report) { + const statusEmoji = { + excellent: '๐Ÿ’š', + good: '๐Ÿ’™', + fair: '๐Ÿ’›', + poor: '๐Ÿงก', + critical: 'โค๏ธ' + }; + + console.log('๐Ÿฅ BMAD Workspace Health Check'); + console.log('=============================='); + console.log(`${statusEmoji[report.status]} Overall Health: ${report.overallScore}/100 (${report.status.toUpperCase()})`); + console.log(`๐Ÿ“Š Issues Found: ${report.summary.totalIssues} (${report.summary.criticalIssues} critical)`); + console.log(`๐Ÿ• Checked: ${new Date(report.timestamp).toLocaleString()}`); + + // Display individual check results + console.log('\n๐Ÿ“‹ Detailed Results:'); + + Object.entries(report.checks).forEach(([checkName, result]) => { + const emoji = result.score >= 90 ? 'โœ…' : result.score >= 70 ? 'โš ๏ธ' : 'โŒ'; + const name = checkName.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase()); + console.log(`${emoji} ${name}: ${result.score}/100`); + + if (result.issues && result.issues.length > 0) { + result.issues.slice(0, 3).forEach(issue => { + console.log(` โ€ข ${issue}`); + }); + if (result.issues.length > 3) { + console.log(` โ€ข ... and ${result.issues.length - 3} more issues`); + } + } + }); + + // Show session summary + if (report.checks.sessionHealth) { + const sessions = report.checks.sessionHealth; + console.log(`\n๐Ÿ‘ฅ Sessions: ${sessions.totalSessions} total, ${sessions.activeSessions} active, ${sessions.staleSessions} stale`); + } + + // Show recommendations + if (report.summary.recommendations.length > 0) { + console.log('\n๐Ÿ’ก Recommendations:'); + report.summary.recommendations.forEach(rec => { + console.log(` โ€ข ${rec}`); + }); + } + + // Quick actions + console.log('\n๐Ÿš€ Quick Actions:'); + console.log(' npm run workspace-cleanup # Repair and optimize workspace'); + console.log(' npm run workspace-status # Check current activity'); + console.log(' npm run workspace-sync # Synchronize context'); + + if (report.overallScore < 70) { + console.log('\nโš ๏ธ Workspace needs attention. Address the issues above for optimal performance.'); + } else if (report.overallScore >= 90) { + console.log('\n๐ŸŽ‰ Excellent! Your workspace is healthy and ready for collaborative development.'); + } +} + +/** + * Main health check function + */ +async function checkWorkspaceHealth(options = {}) { + try { + const workspacePath = path.join(process.cwd(), '.workspace'); + + if (!fs.existsSync(workspacePath)) { + console.error('โŒ Workspace directory not found.'); + console.error(' Run `npm run workspace-init` to initialize workspace'); + process.exit(1); + } + + const report = generateHealthReport(workspacePath); + + if (options.json) { + console.log(JSON.stringify(report, null, 2)); + return; + } + + displayHealthReport(report); + + // Save health report + const reportPath = path.join(workspacePath, 'quality', 'health-report.json'); + const qualityDir = path.dirname(reportPath); + if (!fs.existsSync(qualityDir)) { + fs.mkdirSync(qualityDir, { recursive: true }); + } + fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); + + console.log(`\n๐Ÿ“„ Detailed report saved: .workspace/quality/health-report.json`); + + // Exit with appropriate code for CI/CD + if (options.exitCode && report.summary.criticalIssues > 0) { + process.exit(1); + } + + } catch (error) { + console.error('โŒ Failed to check workspace health:', error.message); + process.exit(1); + } +} + +// Command line execution +if (require.main === module) { + const args = process.argv.slice(2); + const options = { + json: args.includes('--json'), + exitCode: args.includes('--exit-code'), + verbose: args.includes('--verbose') + }; + + checkWorkspaceHealth(options); +} + +module.exports = { checkWorkspaceHealth, generateHealthReport }; \ No newline at end of file diff --git a/workspace-utils/init.js b/workspace-utils/init.js new file mode 100644 index 00000000..3551c713 --- /dev/null +++ b/workspace-utils/init.js @@ -0,0 +1,292 @@ +#!/usr/bin/env node +/** + * BMAD Workspace Initialization Utility + * Cross-IDE workspace initialization with session management + */ + +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +/** + * Detect IDE environment from various sources + */ +function detectIDE() { + // Check environment variables + if (process.env.CURSOR_SOCKET) return 'cursor'; + if (process.env.WINDSURF_SESSION) return 'windsurf'; + if (process.env.TRAE_MODE) return 'trae'; + if (process.env.ROO_CODE) return 'roo'; + if (process.env.CLINE_ACTIVE) return 'cline'; + if (process.env.GEMINI_AI_STUDIO) return 'gemini'; + if (process.env.GITHUB_COPILOT) return 'github-copilot'; + if (process.env.VSCODE_PID) return 'vscode'; + if (process.env.IDE_TYPE) return process.env.IDE_TYPE; + + // Check for IDE-specific files or patterns + if (fs.existsSync('.cursor')) return 'cursor'; + if (fs.existsSync('.windsurf')) return 'windsurf'; + if (fs.existsSync('.vscode')) return 'vscode'; + + return 'unknown'; +} + +/** + * Create workspace directory structure + */ +function createWorkspaceStructure(workspacePath) { + const directories = [ + 'sessions', + 'context', + 'handoffs', + 'decisions', + 'progress', + 'quality', + 'archive', + 'hooks', + 'templates', + 'logs' + ]; + + directories.forEach(dir => { + const dirPath = path.join(workspacePath, dir); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + }); +} + +/** + * Initialize workspace configuration + */ +function initWorkspaceConfig(workspacePath) { + const configPath = path.join(workspacePath, 'workspace-config.json'); + + if (!fs.existsSync(configPath)) { + const config = { + version: '1.0.0', + created: new Date().toISOString(), + lastUpdated: new Date().toISOString(), + features: { + crossIDESupport: true, + sessionManagement: true, + contextPersistence: true, + agentHandoffs: true, + qualityTracking: true + }, + settings: { + maxSessions: 10, + sessionTimeout: 3600000, // 1 hour in milliseconds + autoCleanup: true, + logLevel: 'info' + } + }; + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + return config; + } + + return JSON.parse(fs.readFileSync(configPath, 'utf8')); +} + +/** + * Create session with IDE-specific metadata + */ +function createSession(workspacePath, ide) { + const sessionId = crypto.randomBytes(8).toString('hex'); + const timestamp = new Date().toISOString(); + + const sessionData = { + id: sessionId, + ide: ide, + created: timestamp, + lastHeartbeat: timestamp, + pid: process.pid, + user: process.env.USER || process.env.USERNAME || 'unknown', + cwd: process.cwd(), + nodeVersion: process.version, + platform: process.platform, + arch: process.arch, + metadata: { + ideSpecific: getIDESpecificMetadata(ide), + features: ['context-sharing', 'agent-handoffs', 'quality-tracking'] + } + }; + + const sessionFile = path.join(workspacePath, 'sessions', `${sessionId}.json`); + fs.writeFileSync(sessionFile, JSON.stringify(sessionData, null, 2)); + + return sessionData; +} + +/** + * Get IDE-specific metadata + */ +function getIDESpecificMetadata(ide) { + const metadata = { + supportsTerminalCommands: true, + hasIntegratedGit: false, + supportsPanels: false, + hasExtensionSystem: false + }; + + switch (ide) { + case 'cursor': + metadata.hasIntegratedGit = true; + metadata.supportsPanels = true; + metadata.hasExtensionSystem = true; + metadata.features = ['custom-rules', 'ai-assistance', 'git-integration']; + break; + case 'windsurf': + metadata.hasIntegratedGit = true; + metadata.supportsPanels = true; + metadata.features = ['ai-agent', 'terminal-integration']; + break; + case 'vscode': + metadata.hasIntegratedGit = true; + metadata.supportsPanels = true; + metadata.hasExtensionSystem = true; + metadata.features = ['extensions', 'integrated-terminal', 'git-integration']; + break; + case 'github-copilot': + metadata.hasIntegratedGit = true; + metadata.hasExtensionSystem = true; + metadata.features = ['ai-assistance', 'code-completion']; + break; + default: + metadata.features = ['basic-terminal']; + } + + return metadata; +} + +/** + * Create IDE-specific setup hints + */ +function createIDESetupHints(workspacePath, ide) { + const hintsPath = path.join(workspacePath, 'templates', `${ide}-setup.md`); + + let setupContent = `# ${ide.toUpperCase()} Workspace Setup\n\n`; + + switch (ide) { + case 'cursor': + setupContent += `## Cursor Integration +- Add workspace commands to your terminal +- Use \`npm run workspace-status\` to check collaboration status +- Workspace context is automatically shared between sessions +- Custom rules in .cursor/rules/ will respect workspace state + +## Commands +\`\`\`bash +npm run workspace-init # Initialize session +npm run workspace-status # Check status +npm run workspace-cleanup # Maintenance +\`\`\` +`; + break; + case 'windsurf': + setupContent += `## Windsurf Integration +- Workspace utilities available through terminal +- Context sharing works with Windsurf AI agent +- Session state persists across Windsurf restarts + +## Commands +\`\`\`bash +npm run workspace-init # Start workspace session +npm run workspace-handoff # Prepare agent handoff +npm run workspace-sync # Sync with latest context +\`\`\` +`; + break; + default: + setupContent += `## ${ide.toUpperCase()} Integration +- Use terminal commands for workspace management +- Full workspace functionality available +- Context persists across IDE sessions + +## Available Commands +\`\`\`bash +npm run workspace-init # Initialize workspace session +npm run workspace-status # Show workspace status +npm run workspace-cleanup # Clean and optimize workspace +npm run workspace-handoff # Manage agent handoffs +npm run workspace-sync # Synchronize context +npm run workspace-health # Check workspace health +\`\`\` +`; + } + + if (!fs.existsSync(hintsPath)) { + fs.writeFileSync(hintsPath, setupContent); + } +} + +/** + * Main initialization function + */ +async function initWorkspace() { + try { + const workspacePath = path.join(process.cwd(), '.workspace'); + + // Create workspace directory structure + if (!fs.existsSync(workspacePath)) { + fs.mkdirSync(workspacePath, { recursive: true }); + } + + createWorkspaceStructure(workspacePath); + + // Initialize configuration + const config = initWorkspaceConfig(workspacePath); + + // Detect IDE and create session + const ide = detectIDE(); + const session = createSession(workspacePath, ide); + + // Create IDE-specific setup hints + createIDESetupHints(workspacePath, ide); + + // Log initialization + const logEntry = { + timestamp: new Date().toISOString(), + action: 'workspace-init', + sessionId: session.id, + ide: ide, + user: session.user + }; + + const logPath = path.join(workspacePath, 'logs', 'workspace.log'); + fs.appendFileSync(logPath, JSON.stringify(logEntry) + '\n'); + + // Success output + console.log('โœ… BMAD Workspace initialized successfully'); + console.log('====================================='); + console.log(`๐Ÿ“ Workspace: ${workspacePath}`); + console.log(`๐Ÿ“ Session ID: ${session.id}`); + console.log(`๐Ÿ’ป IDE: ${ide}`); + console.log(`๐Ÿ‘ค User: ${session.user}`); + console.log(`๐Ÿ• Created: ${new Date(session.created).toLocaleString()}`); + + if (ide !== 'unknown') { + console.log(`\n๐Ÿ“– Setup guide: .workspace/templates/${ide}-setup.md`); + } + + console.log('\n๐Ÿš€ Ready for collaborative development!'); + console.log(' โ€ข Run `npm run workspace-status` to check status'); + console.log(' โ€ข Run `npm run workspace-health` for health check'); + + return session.id; + + } catch (error) { + console.error('โŒ Failed to initialize workspace:', error.message); + console.error(' Make sure you have proper file permissions'); + console.error(' Try running from project root directory'); + process.exit(1); + } +} + +// Command line execution +if (require.main === module) { + initWorkspace(); +} + +module.exports = { initWorkspace, detectIDE }; \ No newline at end of file diff --git a/workspace-utils/status.js b/workspace-utils/status.js new file mode 100644 index 00000000..acbbc011 --- /dev/null +++ b/workspace-utils/status.js @@ -0,0 +1,259 @@ +#!/usr/bin/env node +/** + * BMAD Workspace Status Utility + * Cross-IDE workspace status reporting and analytics + */ + +const fs = require('fs'); +const path = require('path'); + +/** + * Get workspace configuration + */ +function getWorkspaceConfig(workspacePath) { + const configPath = path.join(workspacePath, 'workspace-config.json'); + if (fs.existsSync(configPath)) { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } + return null; +} + +/** + * Get active sessions with health check + */ +function getActiveSessions(workspacePath) { + const sessionsPath = path.join(workspacePath, 'sessions'); + if (!fs.existsSync(sessionsPath)) { + return []; + } + + const sessionFiles = fs.readdirSync(sessionsPath).filter(f => f.endsWith('.json')); + const activeSessions = []; + const now = new Date(); + + for (const file of sessionFiles) { + try { + const sessionPath = path.join(sessionsPath, file); + const sessionContent = fs.readFileSync(sessionPath, 'utf8'); + const sessionData = JSON.parse(sessionContent); + + // Check if session is still active (within 1 hour) + const lastHeartbeat = new Date(sessionData.lastHeartbeat); + const timeDiff = now - lastHeartbeat; + const isActive = timeDiff < 3600000; // 1 hour + + sessionData.isActive = isActive; + sessionData.timeSinceLastHeartbeat = timeDiff; + + activeSessions.push(sessionData); + + } catch (e) { + console.warn(`โš ๏ธ Corrupted session file: ${file}`); + } + } + + return activeSessions.sort((a, b) => new Date(b.created) - new Date(a.created)); +} + +/** + * Check workspace health + */ +function checkWorkspaceHealth(workspacePath) { + const requiredDirs = ['sessions', 'context', 'handoffs', 'decisions', 'progress', 'quality']; + const health = { + score: 100, + issues: [], + recommendations: [] + }; + + // Check directory structure + for (const dir of requiredDirs) { + const dirPath = path.join(workspacePath, dir); + if (!fs.existsSync(dirPath)) { + health.score -= 15; + health.issues.push(`Missing directory: ${dir}`); + health.recommendations.push(`Run \`npm run workspace-cleanup\` to repair structure`); + } + } + + // Check for stale sessions + const sessions = getActiveSessions(workspacePath); + const staleSessions = sessions.filter(s => !s.isActive); + if (staleSessions.length > 0) { + health.score -= staleSessions.length * 5; + health.issues.push(`${staleSessions.length} stale sessions detected`); + health.recommendations.push('Run `npm run workspace-cleanup` to remove stale sessions'); + } + + // Check log file size + const logPath = path.join(workspacePath, 'logs', 'workspace.log'); + if (fs.existsSync(logPath)) { + const stats = fs.statSync(logPath); + const logSizeMB = stats.size / (1024 * 1024); + if (logSizeMB > 10) { + health.score -= 10; + health.issues.push(`Large log file: ${logSizeMB.toFixed(1)}MB`); + health.recommendations.push('Consider archiving or rotating log files'); + } + } + + return health; +} + +/** + * Get workspace analytics + */ +function getWorkspaceAnalytics(workspacePath) { + const analytics = { + totalSessions: 0, + activeSessions: 0, + ideBreakdown: {}, + userBreakdown: {}, + avgSessionDuration: 0, + recentActivity: [] + }; + + const sessions = getActiveSessions(workspacePath); + analytics.totalSessions = sessions.length; + analytics.activeSessions = sessions.filter(s => s.isActive).length; + + // IDE breakdown + sessions.forEach(session => { + analytics.ideBreakdown[session.ide] = (analytics.ideBreakdown[session.ide] || 0) + 1; + analytics.userBreakdown[session.user] = (analytics.userBreakdown[session.user] || 0) + 1; + }); + + // Recent activity from logs + const logPath = path.join(workspacePath, 'logs', 'workspace.log'); + if (fs.existsSync(logPath)) { + try { + const logContent = fs.readFileSync(logPath, 'utf8'); + const logLines = logContent.trim().split('\n').slice(-10); // Last 10 entries + + analytics.recentActivity = logLines.map(line => { + try { + return JSON.parse(line); + } catch (e) { + return null; + } + }).filter(Boolean); + } catch (e) { + // Ignore log parsing errors + } + } + + return analytics; +} + +/** + * Format time duration + */ +function formatDuration(milliseconds) { + const seconds = Math.floor(milliseconds / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + if (hours > 0) return `${hours}h ${minutes % 60}m`; + if (minutes > 0) return `${minutes}m ${seconds % 60}s`; + return `${seconds}s`; +} + +/** + * Display workspace status + */ +async function getWorkspaceStatus() { + try { + const workspacePath = path.join(process.cwd(), '.workspace'); + + if (!fs.existsSync(workspacePath)) { + console.error('โŒ Workspace directory not found.'); + console.error(' Run `npm run workspace-init` to initialize workspace'); + process.exit(1); + } + + const config = getWorkspaceConfig(workspacePath); + const sessions = getActiveSessions(workspacePath); + const health = checkWorkspaceHealth(workspacePath); + const analytics = getWorkspaceAnalytics(workspacePath); + + // Header + console.log('๐Ÿค BMAD Collaborative Workspace Status'); + console.log('====================================='); + + // Basic info + console.log(`๐Ÿ“ Workspace: ${workspacePath}`); + console.log(`โš™๏ธ Version: ${config?.version || 'Unknown'}`); + console.log(`๐Ÿ• Created: ${config?.created ? new Date(config.created).toLocaleString() : 'Unknown'}`); + + // Health score + const healthEmoji = health.score >= 90 ? '๐Ÿ’š' : health.score >= 70 ? '๐Ÿ’›' : 'โค๏ธ'; + console.log(`${healthEmoji} Health Score: ${health.score}/100`); + + // Sessions + console.log(`\n๐Ÿ‘ฅ Sessions: ${analytics.totalSessions} total, ${analytics.activeSessions} active`); + + if (sessions.length > 0) { + console.log('\n๐Ÿ“ Session Details:'); + sessions.forEach((session, index) => { + const statusEmoji = session.isActive ? '๐ŸŸข' : '๐ŸŸก'; + const duration = formatDuration(session.timeSinceLastHeartbeat); + console.log(` ${statusEmoji} ${index + 1}. ${session.id} (${session.ide})`); + console.log(` User: ${session.user} | PID: ${session.pid}`); + console.log(` Created: ${new Date(session.created).toLocaleString()}`); + console.log(` Last activity: ${duration} ago`); + + if (session.metadata?.features) { + console.log(` Features: ${session.metadata.features.join(', ')}`); + } + }); + } + + // IDE breakdown + if (Object.keys(analytics.ideBreakdown).length > 0) { + console.log('\n๐Ÿ’ป IDE Usage:'); + Object.entries(analytics.ideBreakdown).forEach(([ide, count]) => { + console.log(` ${ide}: ${count} sessions`); + }); + } + + // Health issues + if (health.issues.length > 0) { + console.log('\nโš ๏ธ Health Issues:'); + health.issues.forEach(issue => console.log(` โ€ข ${issue}`)); + + console.log('\n๐Ÿ’ก Recommendations:'); + health.recommendations.forEach(rec => console.log(` โ€ข ${rec}`)); + } + + // Recent activity + if (analytics.recentActivity.length > 0) { + console.log('\n๐Ÿ“‹ Recent Activity:'); + analytics.recentActivity.slice(-5).forEach(activity => { + const time = new Date(activity.timestamp).toLocaleTimeString(); + console.log(` ${time} - ${activity.action} (${activity.ide || 'unknown'})`); + }); + } + + // Footer + console.log('\n๐Ÿš€ Available Commands:'); + console.log(' npm run workspace-init # Initialize new session'); + console.log(' npm run workspace-cleanup # Clean and repair workspace'); + console.log(' npm run workspace-health # Detailed health check'); + console.log(' npm run workspace-handoff # Manage agent handoffs'); + + if (health.score < 80) { + console.log('\n๐Ÿ’ญ Tip: Run `npm run workspace-cleanup` to improve health score'); + } + + } catch (error) { + console.error('โŒ Failed to get workspace status:', error.message); + process.exit(1); + } +} + +// Command line execution +if (require.main === module) { + getWorkspaceStatus(); +} + +module.exports = { getWorkspaceStatus, getActiveSessions, checkWorkspaceHealth }; \ No newline at end of file diff --git a/workspace-utils/sync.js b/workspace-utils/sync.js new file mode 100644 index 00000000..935adffb --- /dev/null +++ b/workspace-utils/sync.js @@ -0,0 +1,448 @@ +#!/usr/bin/env node +/** + * BMAD Workspace Sync Utility + * Cross-IDE context synchronization and restoration + */ + +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +/** + * Get current session information + */ +function getCurrentSession(workspacePath) { + const sessionsPath = path.join(workspacePath, 'sessions'); + if (!fs.existsSync(sessionsPath)) { + return null; + } + + const sessionFiles = fs.readdirSync(sessionsPath).filter(f => f.endsWith('.json')); + const now = new Date(); + + for (const file of sessionFiles) { + try { + const sessionData = JSON.parse(fs.readFileSync(path.join(sessionsPath, file), 'utf8')); + const lastHeartbeat = new Date(sessionData.lastHeartbeat); + const timeDiff = now - lastHeartbeat; + + // Consider session active if heartbeat within last hour + if (timeDiff < 3600000) { + return sessionData; + } + } catch (e) { + // Skip corrupted session files + } + } + + return null; +} + +/** + * Update session heartbeat + */ +function updateSessionHeartbeat(workspacePath, sessionId) { + const sessionFile = path.join(workspacePath, 'sessions', `${sessionId}.json`); + + if (fs.existsSync(sessionFile)) { + try { + const sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8')); + sessionData.lastHeartbeat = new Date().toISOString(); + fs.writeFileSync(sessionFile, JSON.stringify(sessionData, null, 2)); + return true; + } catch (e) { + console.warn('โš ๏ธ Failed to update session heartbeat:', e.message); + } + } + + return false; +} + +/** + * Sync context from shared workspace + */ +function syncContextFromWorkspace(workspacePath) { + const contextPath = path.join(workspacePath, 'context'); + if (!fs.existsSync(contextPath)) { + return { synced: [], errors: [] }; + } + + const contextFiles = fs.readdirSync(contextPath); + const synced = []; + const errors = []; + + for (const file of contextFiles) { + try { + const filePath = path.join(contextPath, file); + const stats = fs.statSync(filePath); + + if (stats.isFile() && (file.endsWith('.md') || file.endsWith('.json'))) { + // Read context file for validation + const content = fs.readFileSync(filePath, 'utf8'); + + if (content.trim().length > 0) { + synced.push({ + file: file, + size: stats.size, + modified: stats.mtime.toISOString(), + preview: content.substring(0, 100) + (content.length > 100 ? '...' : '') + }); + } + } + } catch (e) { + errors.push(`Failed to sync ${file}: ${e.message}`); + } + } + + return { synced, errors }; +} + +/** + * Get latest progress updates + */ +function getLatestProgress(workspacePath, limit = 5) { + const progressPath = path.join(workspacePath, 'progress'); + if (!fs.existsSync(progressPath)) { + return []; + } + + const progressFiles = fs.readdirSync(progressPath) + .filter(f => f.endsWith('.md')) + .map(f => { + try { + const filePath = path.join(progressPath, f); + const stats = fs.statSync(filePath); + const content = fs.readFileSync(filePath, 'utf8'); + + return { + file: f, + modified: stats.mtime, + size: stats.size, + content: content, + preview: content.substring(0, 150) + (content.length > 150 ? '...' : '') + }; + } catch (e) { + return null; + } + }) + .filter(Boolean) + .sort((a, b) => b.modified - a.modified) + .slice(0, limit); + + return progressFiles; +} + +/** + * Get pending handoffs + */ +function getPendingHandoffs(workspacePath) { + const handoffsPath = path.join(workspacePath, 'handoffs'); + if (!fs.existsSync(handoffsPath)) { + return []; + } + + const handoffFiles = fs.readdirSync(handoffsPath) + .filter(f => f.endsWith('.json')) + .map(f => { + try { + const content = fs.readFileSync(path.join(handoffsPath, f), 'utf8'); + const handoff = JSON.parse(content); + + // Consider handoffs from last 24 hours as potentially relevant + const handoffTime = new Date(handoff.timestamp); + const timeDiff = new Date() - handoffTime; + + if (timeDiff < 86400000) { // 24 hours + return handoff; + } + } catch (e) { + // Skip corrupted handoff files + } + return null; + }) + .filter(Boolean) + .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); + + return handoffFiles; +} + +/** + * Get recent quality reports + */ +function getRecentQualityReports(workspacePath, limit = 3) { + const qualityPath = path.join(workspacePath, 'quality'); + if (!fs.existsSync(qualityPath)) { + return []; + } + + const qualityFiles = fs.readdirSync(qualityPath) + .filter(f => f.endsWith('.json') || f.endsWith('.md')) + .map(f => { + try { + const filePath = path.join(qualityPath, f); + const stats = fs.statSync(filePath); + const content = fs.readFileSync(filePath, 'utf8'); + + return { + file: f, + modified: stats.mtime, + type: f.endsWith('.json') ? 'report' : 'analysis', + preview: content.substring(0, 100) + (content.length > 100 ? '...' : '') + }; + } catch (e) { + return null; + } + }) + .filter(Boolean) + .sort((a, b) => b.modified - a.modified) + .slice(0, limit); + + return qualityFiles; +} + +/** + * Create sync summary + */ +function createSyncSummary(workspacePath, currentSession) { + const contextSync = syncContextFromWorkspace(workspacePath); + const latestProgress = getLatestProgress(workspacePath); + const pendingHandoffs = getPendingHandoffs(workspacePath); + const qualityReports = getRecentQualityReports(workspacePath); + + const summary = { + timestamp: new Date().toISOString(), + session: currentSession, + context: { + filesFound: contextSync.synced.length, + syncErrors: contextSync.errors.length, + files: contextSync.synced + }, + progress: { + recentUpdates: latestProgress.length, + updates: latestProgress + }, + handoffs: { + pending: pendingHandoffs.length, + recent: pendingHandoffs.slice(0, 3) + }, + quality: { + recentReports: qualityReports.length, + reports: qualityReports + } + }; + + return summary; +} + +/** + * Save sync state + */ +function saveSyncState(workspacePath, summary) { + const syncPath = path.join(workspacePath, 'context', 'last-sync.json'); + fs.writeFileSync(syncPath, JSON.stringify(summary, null, 2)); + + // Also create a readable markdown summary + const markdownPath = path.join(workspacePath, 'context', 'sync-summary.md'); + const markdownContent = generateSyncMarkdown(summary); + fs.writeFileSync(markdownPath, markdownContent); + + return { syncFile: syncPath, markdownFile: markdownPath }; +} + +/** + * Generate sync markdown summary + */ +function generateSyncMarkdown(summary) { + const sessionInfo = summary.session ? + `**Current Session:** ${summary.session.id} (${summary.session.ide}) +**User:** ${summary.session.user} +**Last Activity:** ${new Date(summary.session.lastHeartbeat).toLocaleString()}` : + '**No active session found**'; + + return `# Workspace Sync Summary + +**Sync Time:** ${new Date(summary.timestamp).toLocaleString()} + +## Session Information +${sessionInfo} + +## Context Files (${summary.context.filesFound}) +${summary.context.files.length > 0 ? + summary.context.files.map(f => + `- **${f.file}** (${f.size} bytes, modified: ${new Date(f.modified).toLocaleString()}) + ${f.preview}` + ).join('\n\n') : + 'No context files found' +} + +${summary.context.syncErrors.length > 0 ? ` +## Sync Errors +${summary.context.syncErrors.map(e => `- ${e}`).join('\n')} +` : ''} + +## Recent Progress (${summary.progress.recentUpdates}) +${summary.progress.updates.length > 0 ? + summary.progress.updates.map(p => + `- **${p.file}** (${new Date(p.modified).toLocaleString()}) + ${p.preview}` + ).join('\n\n') : + 'No recent progress updates' +} + +## Pending Handoffs (${summary.handoffs.pending}) +${summary.handoffs.recent.length > 0 ? + summary.handoffs.recent.map(h => + `- **${h.id}:** ${h.fromAgent} โ†’ ${h.toAgent} + Work: ${h.currentWork.substring(0, 80)}${h.currentWork.length > 80 ? '...' : ''} + Time: ${new Date(h.timestamp).toLocaleString()}` + ).join('\n\n') : + 'No pending handoffs' +} + +## Quality Reports (${summary.quality.recentReports}) +${summary.quality.reports.length > 0 ? + summary.quality.reports.map(q => + `- **${q.file}** (${q.type}, ${new Date(q.modified).toLocaleString()}) + ${q.preview}` + ).join('\n\n') : + 'No recent quality reports' +} + +--- +*Last synced: ${new Date(summary.timestamp).toLocaleString()}* +*Generated by BMAD Cross-IDE Workspace System* +`; +} + +/** + * Main sync function + */ +async function syncWorkspace(options = {}) { + try { + const workspacePath = path.join(process.cwd(), '.workspace'); + + if (!fs.existsSync(workspacePath)) { + console.error('โŒ Workspace directory not found.'); + console.error(' Run `npm run workspace-init` to initialize workspace'); + process.exit(1); + } + + console.log('๐Ÿ”„ BMAD Workspace Sync'); + console.log('====================='); + console.log(`๐Ÿ“ Workspace: ${workspacePath}`); + + // Get or create session + let currentSession = getCurrentSession(workspacePath); + + if (!currentSession) { + console.log('โš ๏ธ No active session found, checking for workspace initialization...'); + + // Try to find the most recent session + const sessionsPath = path.join(workspacePath, 'sessions'); + if (fs.existsSync(sessionsPath)) { + const sessionFiles = fs.readdirSync(sessionsPath).filter(f => f.endsWith('.json')); + if (sessionFiles.length > 0) { + // Get most recent session + let mostRecent = null; + let mostRecentTime = 0; + + for (const file of sessionFiles) { + try { + const sessionData = JSON.parse(fs.readFileSync(path.join(sessionsPath, file), 'utf8')); + const created = new Date(sessionData.created).getTime(); + if (created > mostRecentTime) { + mostRecentTime = created; + mostRecent = sessionData; + } + } catch (e) { + // Skip corrupted files + } + } + + if (mostRecent) { + console.log(`๐Ÿ“ Using most recent session: ${mostRecent.id} (${mostRecent.ide})`); + currentSession = mostRecent; + } + } + } + + if (!currentSession) { + console.error('โŒ No sessions found. Run `npm run workspace-init` first.'); + process.exit(1); + } + } else { + // Update heartbeat for active session + updateSessionHeartbeat(workspacePath, currentSession.id); + console.log(`โœ… Active session found: ${currentSession.id} (${currentSession.ide})`); + } + + // Create comprehensive sync summary + console.log('\n๐Ÿ” Analyzing workspace state...'); + const summary = createSyncSummary(workspacePath, currentSession); + + // Save sync state + const files = saveSyncState(workspacePath, summary); + + // Log sync activity + const logEntry = { + timestamp: new Date().toISOString(), + action: 'workspace-sync', + sessionId: currentSession.id, + contextFiles: summary.context.filesFound, + progressUpdates: summary.progress.recentUpdates, + pendingHandoffs: summary.handoffs.pending, + user: process.env.USER || process.env.USERNAME || 'unknown' + }; + + const logPath = path.join(workspacePath, 'logs', 'workspace.log'); + fs.appendFileSync(logPath, JSON.stringify(logEntry) + '\n'); + + // Display sync results + console.log('\nโœ… Workspace sync completed'); + console.log('============================'); + console.log(`๐Ÿ“„ Context files: ${summary.context.filesFound}`); + console.log(`๐Ÿ“ˆ Progress updates: ${summary.progress.recentUpdates}`); + console.log(`๐Ÿ”„ Pending handoffs: ${summary.handoffs.pending}`); + console.log(`๐ŸŽฏ Quality reports: ${summary.quality.recentReports}`); + + if (summary.context.syncErrors.length > 0) { + console.log(`\nโš ๏ธ Sync errors: ${summary.context.syncErrors.length}`); + summary.context.syncErrors.forEach(error => console.log(` โ€ข ${error}`)); + } + + console.log(`\n๐Ÿ“ Sync summary: .workspace/context/sync-summary.md`); + console.log(`๐Ÿ“Š Detailed data: .workspace/context/last-sync.json`); + + // Show key highlights + if (summary.handoffs.recent.length > 0) { + console.log('\n๐Ÿ”„ Recent Handoffs:'); + summary.handoffs.recent.slice(0, 2).forEach(handoff => { + console.log(` โ€ข ${handoff.fromAgent} โ†’ ${handoff.toAgent}: ${handoff.currentWork.substring(0, 60)}...`); + }); + } + + if (summary.progress.updates.length > 0) { + console.log('\n๐Ÿ“ˆ Latest Progress:'); + console.log(` โ€ข ${summary.progress.updates[0].file}: ${summary.progress.updates[0].preview}`); + } + + console.log('\n๐Ÿš€ Workspace is now synchronized and ready for collaboration!'); + + } catch (error) { + console.error('โŒ Failed to sync workspace:', error.message); + process.exit(1); + } +} + +// Command line execution +if (require.main === module) { + const args = process.argv.slice(2); + const options = { + verbose: args.includes('--verbose'), + force: args.includes('--force') + }; + + syncWorkspace(options); +} + +module.exports = { syncWorkspace, getCurrentSession, createSyncSummary }; \ No newline at end of file