BMAD-METHOD/tools/installer/lib/workspace-setup.js

1564 lines
52 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const path = require("path");
const fs = require("fs");
const chalk = require("chalk");
class WorkspaceSetup {
constructor() {
this.workspaceStructure = {
'.workspace': {
'sessions': {},
'context': {},
'handoffs': {},
'decisions': {},
'progress': {},
'quality': {},
'archive': {}
}
};
}
async createWorkspaceDirectory(installDir, spinner) {
try {
spinner.text = 'Creating collaborative workspace structure...';
const workspacePath = path.join(installDir, '.workspace');
// Create main workspace directory
if (!fs.existsSync(workspacePath)) {
fs.mkdirSync(workspacePath, { recursive: true });
}
// Create subdirectories
const subdirs = ['sessions', 'context', 'handoffs', 'decisions', 'progress', 'quality', 'archive'];
for (const subdir of subdirs) {
const subdirPath = path.join(workspacePath, subdir);
if (!fs.existsSync(subdirPath)) {
fs.mkdirSync(subdirPath, { recursive: true });
}
}
// Create initial workspace configuration
const workspaceConfig = {
version: "1.0",
created: new Date().toISOString(),
structure: subdirs,
settings: {
maxContextSize: "10MB",
sessionTimeout: "2h",
archiveAfter: "30d",
maxConcurrentSessions: 5
}
};
fs.writeFileSync(
path.join(workspacePath, 'workspace-config.json'),
JSON.stringify(workspaceConfig, null, 2)
);
// Create initial README
const readmeContent = `# BMAD Collaborative Workspace
This directory contains the collaborative workspace system for multi-session AI agent coordination.
## Directory Structure
- \`sessions/\` - Active session tracking
- \`context/\` - Shared context files and decisions
- \`handoffs/\` - Agent transition packages
- \`decisions/\` - Architectural and design decisions
- \`progress/\` - Story and task progress tracking
- \`quality/\` - Quality metrics and audit results
- \`archive/\` - Compressed historical context
## Usage
### Claude Code CLI Users
- Use \`*workspace-init\` to initialize a collaborative session
- Use \`*workspace-status\` to see active sessions and progress
- Use \`*workspace-cleanup\` for maintenance
### Other IDE Users
- Run \`npm run workspace-init\` to initialize
- Run \`npm run workspace-status\` for status
- Run \`npm run workspace-cleanup\` for maintenance
## Configuration
Workspace settings can be modified in \`workspace-config.json\`.
`;
fs.writeFileSync(path.join(workspacePath, 'README.md'), readmeContent);
return true;
} catch (error) {
console.error(chalk.red('Failed to create workspace directory:'), error.message);
return false;
}
}
async createWorkspaceUtilities(installDir, selectedIDEs, spinner) {
try {
spinner.text = 'Installing workspace utilities...';
const utilsPath = path.join(installDir, 'workspace-utils');
if (!fs.existsSync(utilsPath)) {
fs.mkdirSync(utilsPath, { recursive: true });
}
// Create utility scripts
await this.createInitScript(utilsPath);
await this.createStatusScript(utilsPath);
await this.createCleanupScript(utilsPath);
await this.createHandoffScript(utilsPath);
await this.createSyncScript(utilsPath);
await this.createContextScript(utilsPath);
// Create package.json scripts if package.json exists
await this.addPackageJsonScripts(installDir);
// Create IDE-specific documentation
await this.createIDEDocumentation(utilsPath, selectedIDEs);
return true;
} catch (error) {
console.error(chalk.red('Failed to create workspace utilities:'), error.message);
return false;
}
}
async createInitScript(utilsPath) {
const initScript = `#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
async function initWorkspace() {
try {
const workspacePath = path.join(process.cwd(), '.workspace');
if (!fs.existsSync(workspacePath)) {
console.error('❌ Workspace directory not found. Run \`npx bmad-method install\` first.');
process.exit(1);
}
// Generate session ID
const sessionId = crypto.randomBytes(8).toString('hex');
const timestamp = new Date().toISOString();
// Create session file
const sessionData = {
id: sessionId,
created: timestamp,
lastHeartbeat: timestamp,
ide: process.env.IDE_TYPE || 'unknown',
pid: process.pid,
user: process.env.USER || process.env.USERNAME || 'unknown'
};
const sessionsPath = path.join(workspacePath, 'sessions');
if (!fs.existsSync(sessionsPath)) {
fs.mkdirSync(sessionsPath, { recursive: true });
}
const sessionFile = path.join(sessionsPath, \`\${sessionId}.json\`);
fs.writeFileSync(sessionFile, JSON.stringify(sessionData, null, 2));
console.log('✅ Workspace initialized successfully');
console.log(\`📍 Session ID: \${sessionId}\`);
console.log(\`🕐 Created: \${timestamp}\`);
return sessionId;
} catch (error) {
console.error('❌ Failed to initialize workspace:', error.message);
process.exit(1);
}
}
if (require.main === module) {
initWorkspace();
}
module.exports = { initWorkspace };
`;
fs.writeFileSync(path.join(utilsPath, 'init.js'), initScript);
fs.chmodSync(path.join(utilsPath, 'init.js'), 0o755);
}
async createStatusScript(utilsPath) {
const statusScript = `#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
async function getWorkspaceStatus() {
try {
const workspacePath = path.join(process.cwd(), '.workspace');
if (!fs.existsSync(workspacePath)) {
console.error('❌ Workspace directory not found.');
process.exit(1);
}
// Read workspace config
const configPath = path.join(workspacePath, 'workspace-config.json');
let config = {};
if (fs.existsSync(configPath)) {
const configContent = fs.readFileSync(configPath, 'utf8');
config = JSON.parse(configContent);
}
// Get active sessions
const sessionsPath = path.join(workspacePath, 'sessions');
let sessionFiles = [];
if (fs.existsSync(sessionsPath)) {
sessionFiles = fs.readdirSync(sessionsPath);
}
const activeSessions = [];
for (const file of sessionFiles) {
if (file.endsWith('.json')) {
try {
const sessionPath = path.join(sessionsPath, file);
const sessionContent = fs.readFileSync(sessionPath, 'utf8');
const sessionData = JSON.parse(sessionContent);
activeSessions.push(sessionData);
} catch (e) {
// Skip corrupted session files
}
}
}
// Display status
console.log('🤝 BMAD Collaborative Workspace Status');
console.log('=====================================');
console.log(\`📁 Workspace: \${workspacePath}\`);
console.log(\`⚙️ Version: \${config.version || 'Unknown'}\`);
console.log(\`🕐 Created: \${config.created || 'Unknown'}\`);
console.log(\`👥 Active Sessions: \${activeSessions.length}\`);
if (activeSessions.length > 0) {
console.log('\\n📍 Session Details:');
activeSessions.forEach((session, index) => {
console.log(\` \${index + 1}. \${session.id} (\${session.ide}) - \${session.user}\`);
console.log(\` Created: \${new Date(session.created).toLocaleString()}\`);
console.log(\` Last Heartbeat: \${new Date(session.lastHeartbeat).toLocaleString()}\`);
});
}
// Check directory structure
const directories = ['context', 'handoffs', 'decisions', 'progress', 'quality', 'archive'];
const missingDirs = [];
for (const dir of directories) {
if (!fs.existsSync(path.join(workspacePath, dir))) {
missingDirs.push(dir);
}
}
if (missingDirs.length > 0) {
console.log(\`\\n⚠ Missing directories: \${missingDirs.join(', ')}\`);
console.log(' Run \`node workspace-utils/cleanup.js\` to repair.');
} else {
console.log('\\n✅ Workspace structure is healthy');
}
} catch (error) {
console.error('❌ Failed to get workspace status:', error.message);
process.exit(1);
}
}
if (require.main === module) {
getWorkspaceStatus();
}
module.exports = { getWorkspaceStatus };
`;
fs.writeFileSync(path.join(utilsPath, 'status.js'), statusScript);
fs.chmodSync(path.join(utilsPath, 'status.js'), 0o755);
}
async createCleanupScript(utilsPath) {
const cleanupScript = `#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
function ensureDir(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
function removeFile(filePath) {
try {
fs.unlinkSync(filePath);
return true;
} catch (e) {
return false;
}
}
function moveFile(sourcePath, targetPath) {
try {
const data = fs.readFileSync(sourcePath);
fs.writeFileSync(targetPath, data);
fs.unlinkSync(sourcePath);
return true;
} catch (e) {
return false;
}
}
async function cleanupWorkspace() {
try {
const workspacePath = path.join(process.cwd(), '.workspace');
if (!fs.existsSync(workspacePath)) {
console.error('❌ Workspace directory not found.');
process.exit(1);
}
console.log('🧹 Starting workspace cleanup...');
// Repair directory structure
const directories = ['sessions', 'context', 'handoffs', 'decisions', 'progress', 'quality', 'archive'];
let repairedDirs = 0;
for (const dir of directories) {
const dirPath = path.join(workspacePath, dir);
if (!fs.existsSync(dirPath)) {
ensureDir(dirPath);
repairedDirs++;
}
}
if (repairedDirs > 0) {
console.log(\`✅ Repaired \${repairedDirs} missing directories\`);
}
// Clean up expired sessions (older than 2 hours)
const sessionsPath = path.join(workspacePath, 'sessions');
let sessionFiles = [];
if (fs.existsSync(sessionsPath)) {
sessionFiles = fs.readdirSync(sessionsPath);
}
const twoHoursAgo = Date.now() - (2 * 60 * 60 * 1000);
let cleanedSessions = 0;
for (const file of sessionFiles) {
if (file.endsWith('.json')) {
try {
const sessionPath = path.join(sessionsPath, file);
const sessionContent = fs.readFileSync(sessionPath, 'utf8');
const sessionData = JSON.parse(sessionContent);
const lastHeartbeat = new Date(sessionData.lastHeartbeat).getTime();
if (lastHeartbeat < twoHoursAgo) {
if (removeFile(sessionPath)) {
cleanedSessions++;
}
}
} catch (e) {
// Remove corrupted session files
if (removeFile(path.join(sessionsPath, file))) {
cleanedSessions++;
}
}
}
}
if (cleanedSessions > 0) {
console.log(\`✅ Cleaned up \${cleanedSessions} expired sessions\`);
}
// Archive old context files (older than 30 days)
const contextPath = path.join(workspacePath, 'context');
const archivePath = path.join(workspacePath, 'archive');
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
if (fs.existsSync(contextPath)) {
let contextFiles = [];
try {
contextFiles = fs.readdirSync(contextPath);
} catch (e) {
contextFiles = [];
}
let archivedFiles = 0;
for (const file of contextFiles) {
const filePath = path.join(contextPath, file);
try {
const stats = fs.statSync(filePath);
if (stats.mtime.getTime() < thirtyDaysAgo) {
const archiveFile = path.join(archivePath, \`archived-\${Date.now()}-\${file}\`);
if (moveFile(filePath, archiveFile)) {
archivedFiles++;
}
}
} catch (e) {
// Skip files that can't be processed
}
}
if (archivedFiles > 0) {
console.log(\`✅ Archived \${archivedFiles} old context files\`);
}
}
console.log('✅ Workspace cleanup completed successfully');
} catch (error) {
console.error('❌ Failed to cleanup workspace:', error.message);
process.exit(1);
}
}
if (require.main === module) {
cleanupWorkspace();
}
module.exports = { cleanupWorkspace };
`;
fs.writeFileSync(path.join(utilsPath, 'cleanup.js'), cleanupScript);
fs.chmodSync(path.join(utilsPath, 'cleanup.js'), 0o755);
}
async createHandoffScript(utilsPath) {
const handoffScript = `#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
// Embedded HandoffManager functionality using only built-in modules
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');
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']
}
};
this.initialize();
}
initialize() {
if (!fs.existsSync(this.handoffsPath)) {
fs.mkdirSync(this.handoffsPath, { recursive: true });
}
}
getAgentType(agentName) {
const lowerName = agentName.toLowerCase();
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';
return 'dev'; // Default fallback
}
async loadWorkspaceContext() {
const context = { shared: {}, decisions: [], progress: {}, quality: {} };
try {
// Load shared context
const sharedContextFile = path.join(this.contextPath, 'shared-context.md');
if (fs.existsSync(sharedContextFile)) {
const content = fs.readFileSync(sharedContextFile, 'utf8');
context.shared = this.parseSharedContext(content);
}
// Load decisions
const decisionsFile = path.join(this.workspacePath, 'decisions', 'decisions-log.md');
if (fs.existsSync(decisionsFile)) {
const content = fs.readFileSync(decisionsFile, 'utf8');
context.decisions = this.parseDecisions(content);
}
// Load progress
const progressFile = path.join(this.workspacePath, 'progress', 'progress-summary.md');
if (fs.existsSync(progressFile)) {
const content = fs.readFileSync(progressFile, 'utf8');
context.progress = this.parseProgress(content);
}
} catch (error) {
console.warn('Warning: Could not load full workspace context:', error.message);
}
return context;
}
parseSharedContext(content) {
const context = {};
try {
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);
}
} 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 <= 5; i++) {
try {
const block = decisionBlocks[i];
const lines = block.split('\\n');
const decision = {
title: lines[0].trim(),
agent: this.extractField(block, 'Agent'),
decision: this.extractField(block, 'Decision'),
rationale: this.extractField(block, 'Rationale')
};
decisions.push(decision);
} catch (error) {
console.warn(\`Failed to parse decision block \${i}:\`, error.message);
}
}
return decisions;
}
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];
} catch (error) {
console.warn('Failed to parse progress:', error.message);
}
return progress;
}
extractField(content, fieldName) {
const regex = new RegExp(\`\\\\*\\\\*\${fieldName}:\\\\*\\\\* (.+)\`, 'i');
const match = content.match(regex);
return match ? match[1].trim() : '';
}
generateNextActions(context, agentType) {
const 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');
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');
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');
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');
break;
default:
actions.push('Review handoff context and understand current state');
actions.push('Identify specific tasks relevant to your role');
}
// 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, 6);
}
async createHandoff(sourceAgent, targetAgent, customContext = '') {
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
const context = await this.loadWorkspaceContext();
const agentType = this.getAgentType(targetAgent);
const nextActions = this.generateNextActions(context, agentType);
const handoffContent = \`# 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 || ''}
## Key Decisions Made
\${context.decisions.map(d => \`- **\${d.title}** (\${d.agent}): \${d.decision}\`).join('\\n') || '- No relevant decisions available'}
## Current Progress
**Story:** \${context.progress.currentStory || 'No active story'}
**Quality Score:** \${context.progress.qualityScore || 'Not assessed'}
## Next Actions for \${targetAgent}
\${nextActions.map(action => \`- [ ] \${action}\`).join('\\n')}
## Files and References
- 📁 \`.workspace/context/shared-context.md\` - Current workspace context
- 📋 \`.workspace/decisions/decisions-log.md\` - Architectural decisions
- 📈 \`.workspace/progress/progress-summary.md\` - Development progress
- 📊 \`.workspace/quality/quality-metrics.md\` - Quality assessments
## Blockers and Dependencies
- Review workspace context for any identified blockers
- Check progress summary for pending dependencies
## Handoff Validation
- [ ] Context completeness verified
- [ ] Decisions documented and relevant to \${agentType}
- [ ] Next actions clearly defined for \${agentType} role
- [ ] References included
- [ ] Agent-specific filtering applied
## Handoff Notes
Generated automatically with agent-specific context filtering for \${agentType} role.
---
*Generated by BMAD Agent Handoff System v1.3*
\`;
fs.writeFileSync(handoffFile, handoffContent);
// Update registry
await this.updateHandoffRegistry(handoffId, sourceAgent, targetAgent);
return {
handoffId,
filePath: handoffFile,
success: true
};
} catch (error) {
console.error('Failed to create handoff:', error.message);
throw error;
}
}
async updateHandoffRegistry(handoffId, sourceAgent, targetAgent) {
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(),
status: 'pending'
});
// Keep only last 50 handoffs
if (registry.length > 50) {
registry = registry.slice(-50);
}
fs.writeFileSync(registryFile, JSON.stringify(registry, null, 2));
} catch (error) {
console.error('Failed to update handoff registry:', 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 [];
}
}
}
// CLI Interface
async function handleHandoffCommand() {
try {
const workspacePath = path.join(process.cwd(), '.workspace');
if (!fs.existsSync(workspacePath)) {
console.error('❌ Workspace directory not found. Run \`npx bmad-method install\` first.');
process.exit(1);
}
const handoffManager = new HandoffManager();
const args = process.argv.slice(2);
const command = args[0];
switch (command) {
case 'create':
if (args.length < 3) {
console.log('Usage: node handoff.js create <from-agent> <to-agent> [context]');
process.exit(1);
}
await createHandoff(handoffManager, args[1], args[2], args[3] || '');
break;
case 'list':
await listPendingHandoffs(handoffManager, args[1]);
break;
case 'status':
await showHandoffStatus(handoffManager);
break;
default:
// Backward compatibility - if first arg looks like agent name
if (args.length >= 2) {
await createHandoff(handoffManager, args[0], args[1], args[2] || '');
} else {
showUsage();
}
}
} catch (error) {
console.error('❌ Handoff command failed:', error.message);
process.exit(1);
}
}
async function createHandoff(handoffManager, fromAgent, toAgent, context) {
try {
const result = await handoffManager.createHandoff(fromAgent, toAgent, context);
console.log('✅ Enhanced handoff package created successfully');
console.log(\`📦 Handoff ID: \${result.handoffId}\`);
console.log(\`📁 File: \${result.filePath}\`);
console.log(\`🎯 Target Agent Type: \${handoffManager.getAgentType(toAgent)}\`);
console.log(\`📄 Context loaded from workspace automatically\`);
} catch (error) {
console.error('❌ Failed to create handoff:', error.message);
process.exit(1);
}
}
async function listPendingHandoffs(handoffManager, targetAgent) {
try {
const pending = await handoffManager.getPendingHandoffs(targetAgent);
if (pending.length === 0) {
console.log(\`📋 No pending handoffs\${targetAgent ? \` for \${targetAgent}\` : ''}\`);
return;
}
console.log(\`📋 Pending Handoffs\${targetAgent ? \` for \${targetAgent}\` : ''}\`);
console.log('='.repeat(50));
pending.forEach((handoff, index) => {
console.log(\`\${index + 1}. \${handoff.handoffId}\`);
console.log(\` From: \${handoff.sourceAgent} → To: \${handoff.targetAgent}\`);
console.log(\` Created: \${new Date(handoff.timestamp).toLocaleString()}\`);
console.log('');
});
} catch (error) {
console.error('❌ Failed to list handoffs:', error.message);
}
}
async function showHandoffStatus(handoffManager) {
try {
const registryFile = path.join(handoffManager.handoffsPath, 'handoff-registry.json');
if (!fs.existsSync(registryFile)) {
console.log('📋 No handoffs created yet.');
return;
}
const content = fs.readFileSync(registryFile, 'utf8');
const registry = JSON.parse(content);
console.log('📊 Handoff System Status');
console.log('========================');
console.log(\`📁 Handoffs Directory: \${handoffManager.handoffsPath}\`);
console.log(\`📋 Total Handoffs: \${registry.length}\`);
console.log(\`⏳ Pending Handoffs: \${registry.filter(h => h.status === 'pending').length}\`);
if (registry.length > 0) {
console.log('\\n📈 Recent Activity:');
registry.slice(-3).forEach((handoff, index) => {
console.log(\` \${index + 1}. \${handoff.sourceAgent} → \${handoff.targetAgent}\`);
console.log(\` \${new Date(handoff.timestamp).toLocaleString()}\`);
});
}
} catch (error) {
console.error('❌ Failed to show handoff status:', error.message);
}
}
function showUsage() {
console.log('🤝 BMAD Agent Handoff System');
console.log('=============================');
console.log('');
console.log('Usage: node handoff.js <command> [options]');
console.log('');
console.log('Commands:');
console.log(' create <from> <to> [context] - Create handoff package with workspace context');
console.log(' list [agent] - List pending handoffs (optionally filtered by target agent)');
console.log(' status - Show handoff system status');
console.log('');
console.log('Examples:');
console.log(' node handoff.js create dev qa "Ready for testing"');
console.log(' node handoff.js list qa');
console.log(' node handoff.js status');
console.log('');
console.log('Backward compatibility:');
console.log(' node handoff.js <from-agent> <to-agent> [context]');
}
if (require.main === module) {
handleHandoffCommand();
}
module.exports = { HandoffManager };
`;
fs.writeFileSync(path.join(utilsPath, 'handoff.js'), handoffScript);
fs.chmodSync(path.join(utilsPath, 'handoff.js'), 0o755);
}
async createSyncScript(utilsPath) {
const syncScript = `#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
async function syncWorkspace() {
try {
const workspacePath = path.join(process.cwd(), '.workspace');
if (!fs.existsSync(workspacePath)) {
console.error('❌ Workspace directory not found.');
process.exit(1);
}
console.log('🔄 Synchronizing workspace context...');
// Update session heartbeat
const sessionsPath = path.join(workspacePath, 'sessions');
let sessionFiles = [];
if (fs.existsSync(sessionsPath)) {
try {
sessionFiles = fs.readdirSync(sessionsPath);
} catch (e) {
sessionFiles = [];
}
}
// For simplicity, update the most recent session
let latestSession = null;
let latestTime = 0;
for (const file of sessionFiles) {
if (file.endsWith('.json')) {
try {
const sessionPath = path.join(sessionsPath, file);
const sessionContent = fs.readFileSync(sessionPath, 'utf8');
const sessionData = JSON.parse(sessionContent);
const created = new Date(sessionData.created).getTime();
if (created > latestTime) {
latestTime = created;
latestSession = { path: sessionPath, data: sessionData };
}
} catch (e) {
// Skip corrupted files
}
}
}
if (latestSession) {
latestSession.data.lastHeartbeat = new Date().toISOString();
fs.writeFileSync(latestSession.path, JSON.stringify(latestSession.data, null, 2));
console.log(\`✅ Updated session heartbeat: \${latestSession.data.id}\`);
}
// Load and display recent context
const contextPath = path.join(workspacePath, 'context');
const sharedContext = path.join(contextPath, 'shared-context.md');
if (fs.existsSync(sharedContext)) {
try {
const content = fs.readFileSync(sharedContext, 'utf8');
console.log('\\n📄 Current Shared Context:');
console.log('='.repeat(50));
console.log(content.substring(0, 500) + (content.length > 500 ? '...' : ''));
} catch (e) {
console.log('\\n📄 Shared context file exists but could not be read.');
}
} else {
console.log('\\n📄 No shared context available yet.');
}
console.log('\\n✅ Workspace synchronization completed');
} catch (error) {
console.error('❌ Failed to sync workspace:', error.message);
process.exit(1);
}
}
if (require.main === module) {
syncWorkspace();
}
module.exports = { syncWorkspace };
`;
fs.writeFileSync(path.join(utilsPath, 'sync.js'), syncScript);
fs.chmodSync(path.join(utilsPath, 'sync.js'), 0o755);
}
async createContextScript(utilsPath) {
const contextScript = `#!/usr/bin/env node
const path = require('path');
const fs = require('fs');
// Context Management CLI
async function handleContextCommand() {
try {
const workspacePath = path.join(process.cwd(), '.workspace');
if (!fs.existsSync(workspacePath)) {
console.error('❌ Workspace directory not found. Run \`npx bmad-method install\` first.');
process.exit(1);
}
const args = process.argv.slice(2);
const command = args[0];
switch (command) {
case 'status':
await showContextStatus();
break;
case 'load':
await loadContext();
break;
case 'decisions':
await showDecisions();
break;
case 'progress':
await showProgress();
break;
case 'export':
await exportContext();
break;
default:
showUsage();
}
} catch (error) {
console.error('❌ Context command failed:', error.message);
process.exit(1);
}
}
async function showContextStatus() {
const workspacePath = path.join(process.cwd(), '.workspace');
const contextPath = path.join(workspacePath, 'context');
const contextFile = path.join(contextPath, 'shared-context.md');
console.log('📄 BMAD Context Status');
console.log('======================');
console.log(\`📁 Context: \${contextPath}\`);
if (fs.existsSync(contextFile)) {
const content = fs.readFileSync(contextFile, 'utf8');
const lastUpdatedMatch = content.match(/\\*\\*Last Updated:\\*\\* (.+)/);
const primaryAgentMatch = content.match(/\\*\\*Primary Agent:\\*\\* (.+)/);
const currentFocusMatch = content.match(/## Current Focus\\n([\\s\\S]*?)(?=\\n## |$)/);
console.log(\`🕐 Last Updated: \${lastUpdatedMatch ? lastUpdatedMatch[1] : 'Unknown'}\`);
console.log(\`👤 Primary Agent: \${primaryAgentMatch ? primaryAgentMatch[1] : 'Unknown'}\`);
console.log(\`🎯 Current Focus: \${currentFocusMatch ? currentFocusMatch[1].trim() : 'No focus set'}\`);
} else {
console.log('📄 No shared context available yet.');
console.log('💡 Context will be created when agents start working.');
}
// Check for other context files
const decisionsFile = path.join(workspacePath, 'decisions', 'decisions-log.md');
const progressFile = path.join(workspacePath, 'progress', 'progress-summary.md');
const qualityFile = path.join(workspacePath, 'quality', 'quality-metrics.md');
console.log(\`\\n📋 Decisions Log: \${fs.existsSync(decisionsFile) ? 'Available' : 'Not created yet'}\`);
console.log(\`📈 Progress Summary: \${fs.existsSync(progressFile) ? 'Available' : 'Not created yet'}\`);
console.log(\`📊 Quality Metrics: \${fs.existsSync(qualityFile) ? 'Available' : 'Not created yet'}\`);
}
async function loadContext() {
const contextFile = path.join(process.cwd(), '.workspace', 'context', 'shared-context.md');
if (!fs.existsSync(contextFile)) {
console.log('📄 No shared context available yet.');
console.log('💡 Context will be created when agents start working.');
return;
}
console.log('📄 Loading workspace context...\\n');
const content = fs.readFileSync(contextFile, 'utf8');
console.log(content);
}
async function showDecisions() {
const decisionsFile = path.join(process.cwd(), '.workspace', 'decisions', 'decisions-log.md');
if (!fs.existsSync(decisionsFile)) {
console.log('📋 No decisions recorded yet.');
console.log('💡 Decisions will be logged as agents make architectural choices.');
return;
}
console.log('📋 Recent Architectural & Design Decisions');
console.log('==========================================');
const content = fs.readFileSync(decisionsFile, 'utf8');
const decisions = content.split('## Decision ').slice(1);
if (decisions.length === 0) {
console.log('📋 No decisions recorded yet.');
return;
}
// Show last 5 decisions
decisions.slice(-5).forEach((decision, index) => {
const lines = decision.split('\\n');
const title = lines[0].replace(/^\\d+:\\s*/, '');
const dateMatch = decision.match(/\\*\\*Date:\\*\\* (.+)/);
const agentMatch = decision.match(/\\*\\*Agent:\\*\\* (.+)/);
console.log(\`\\n\${decisions.length - 4 + index}. \${title}\`);
if (dateMatch) console.log(\` 📅 \${dateMatch[1]}\`);
if (agentMatch) console.log(\` 👤 \${agentMatch[1]}\`);
});
}
async function showProgress() {
const progressFile = path.join(process.cwd(), '.workspace', 'progress', 'progress-summary.md');
if (!fs.existsSync(progressFile)) {
console.log('📈 No progress tracking available yet.');
console.log('💡 Progress will be tracked as agents work on stories.');
return;
}
console.log('📈 Development Progress');
console.log('======================');
const content = fs.readFileSync(progressFile, 'utf8');
console.log(content);
}
async function exportContext() {
try {
const workspacePath = path.join(process.cwd(), '.workspace');
const timestamp = new Date().toISOString();
let exportContent = \`# Workspace Context Export\\n**Generated:** \${timestamp}\\n\\n\`;
// Add shared context
const contextFile = path.join(workspacePath, 'context', 'shared-context.md');
if (fs.existsSync(contextFile)) {
exportContent += '## Shared Context\\n';
exportContent += fs.readFileSync(contextFile, 'utf8') + '\\n\\n';
}
// Add recent decisions
const decisionsFile = path.join(workspacePath, 'decisions', 'decisions-log.md');
if (fs.existsSync(decisionsFile)) {
exportContent += '## Recent Decisions\\n';
const decisionsContent = fs.readFileSync(decisionsFile, 'utf8');
const decisions = decisionsContent.split('## Decision ').slice(-3);
exportContent += decisions.join('## Decision ') + '\\n\\n';
}
// Add progress summary
const progressFile = path.join(workspacePath, 'progress', 'progress-summary.md');
if (fs.existsSync(progressFile)) {
exportContent += '## Progress Summary\\n';
exportContent += fs.readFileSync(progressFile, 'utf8') + '\\n\\n';
}
const exportFile = path.join(process.cwd(), \`context-export-\${Date.now()}.md\`);
fs.writeFileSync(exportFile, exportContent);
console.log('✅ Context exported successfully');
console.log(\`📁 Export file: \${exportFile}\`);
} catch (error) {
console.error('❌ Failed to export context:', error.message);
}
}
function showUsage() {
console.log('📄 BMAD Context Management');
console.log('==========================');
console.log('');
console.log('Usage: node context.js <command>');
console.log('');
console.log('Commands:');
console.log(' status - Show current workspace context status');
console.log(' load - Load and display shared context');
console.log(' decisions - Show recent architectural decisions');
console.log(' progress - Show development progress summary');
console.log(' export - Export context to markdown file');
console.log('');
console.log('Examples:');
console.log(' node workspace-utils/context.js status');
console.log(' npm run workspace-context status');
console.log(' node workspace-utils/context.js export');
}
if (require.main === module) {
handleContextCommand();
}
`;
fs.writeFileSync(path.join(utilsPath, 'context.js'), contextScript);
fs.chmodSync(path.join(utilsPath, 'context.js'), 0o755);
}
async addPackageJsonScripts(installDir) {
const packageJsonPath = path.join(installDir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonContent);
if (!packageJson.scripts) {
packageJson.scripts = {};
}
// Add workspace scripts
packageJson.scripts['workspace-init'] = 'node workspace-utils/init.js';
packageJson.scripts['workspace-status'] = 'node workspace-utils/status.js';
packageJson.scripts['workspace-cleanup'] = 'node workspace-utils/cleanup.js';
packageJson.scripts['workspace-handoff'] = 'node workspace-utils/handoff.js';
packageJson.scripts['workspace-sync'] = 'node workspace-utils/sync.js';
packageJson.scripts['workspace-context'] = 'node workspace-utils/context.js';
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
}
}
async createIDEDocumentation(utilsPath, selectedIDEs) {
const docsPath = path.join(utilsPath, 'docs');
if (!fs.existsSync(docsPath)) {
fs.mkdirSync(docsPath, { recursive: true });
}
const ideDocuments = {
'cursor': `# Workspace Usage in Cursor
## Getting Started
1. Open terminal in Cursor
2. Run \`node workspace-utils/init.js\` to start collaborative session
3. Use \`node workspace-utils/status.js\` to see active sessions
## Best Practices
- Use @dev, @qa, @architect mentions to invoke BMAD agents
- Run \`node workspace-utils/sync.js\` before major context switches
- Check \`node workspace-utils/status.js\` to see other team members' progress
`,
'windsurf': `# Workspace Usage in Windsurf
## Getting Started
1. Open terminal in Windsurf
2. Run \`node workspace-utils/init.js\` to start collaborative session
3. Use \`node workspace-utils/status.js\` to see active sessions
## Best Practices
- Use @agent-name to invoke BMAD agents
- Run \`node workspace-utils/sync.js\` to stay synchronized
- Check workspace status regularly for team coordination
`,
'claude-code': `# Workspace Usage in Claude Code CLI
## Getting Started
Claude Code CLI users get enhanced workspace experience with native commands:
- \`*workspace-init\` - Initialize collaborative session (automatic)
- \`*workspace-status\` - Show active sessions and progress
- \`*workspace-cleanup\` - Clean up and optimize workspace
- \`*workspace-handoff [agent]\` - Prepare handoff to another agent
- \`*workspace-sync\` - Synchronize with latest context
## Native Integration
Workspace features are automatically integrated into your Claude Code CLI session:
- Automatic session registration and heartbeat
- Context-aware agent handoffs
- Intelligent workspace suggestions
`,
'trae': `# Workspace Usage in Trae
## Getting Started
1. Open terminal in Trae
2. Run \`node workspace-utils/init.js\` to start collaborative session
3. Use \`node workspace-utils/status.js\` to see active sessions
## Integration
- Use @agent mentions to work with BMAD agents
- Workspace context automatically persists across sessions
- Use \`node workspace-utils/handoff.js dev qa\` for explicit handoffs
`
};
for (const ide of selectedIDEs) {
if (ideDocuments[ide]) {
fs.writeFileSync(
path.join(docsPath, `${ide}.md`),
ideDocuments[ide]
);
}
}
}
async setupClaudeCodeWorkspaceCommands(installDir, spinner) {
try {
spinner.text = 'Integrating workspace commands with Claude Code CLI agents...';
const bmadCorePath = path.join(installDir, '.bmad-core');
const agentsPath = path.join(bmadCorePath, 'agents');
if (!fs.existsSync(agentsPath)) {
console.warn('⚠️ .bmad-core/agents directory not found. Skipping Claude Code integration.');
return false;
}
// Add workspace commands to key agents
const agentsToUpdate = ['dev.md', 'qa.md', 'sm.md', 'analyst.md', 'architect.md', 'ux-expert.md', 'pm.md', 'po.md'];
for (const agentFile of agentsToUpdate) {
const agentPath = path.join(agentsPath, agentFile);
if (fs.existsSync(agentPath)) {
let content = fs.readFileSync(agentPath, 'utf8');
// Check if workspace commands already exist
if (!content.includes('*workspace-init')) {
// Add workspace commands section
const workspaceCommands = `
## Workspace Commands
You have access to collaborative workspace commands for multi-session coordination:
- \`*workspace-init\` - Initialize collaborative workspace session
- \`*workspace-status\` - Show current workspace status and active sessions
- \`*workspace-cleanup\` - Clean up workspace files and optimize storage
- \`*workspace-handoff [target-agent]\` - Prepare context handoff to specified agent
- \`*workspace-sync\` - Synchronize with latest workspace context
Use these commands to coordinate with other AI agents and maintain context across sessions.
`;
// Insert before the last section (usually before final instructions)
const insertPoint = content.lastIndexOf('\n## ');
if (insertPoint > -1) {
content = content.slice(0, insertPoint) + workspaceCommands + '\n' + content.slice(insertPoint);
} else {
content += workspaceCommands;
}
fs.writeFileSync(agentPath, content);
}
}
}
// Install Claude Code CLI optimization modules
spinner.text = 'Installing Claude Code CLI optimization features...';
await this.installClaudeCodeOptimizations(installDir);
return true;
} catch (error) {
console.error(chalk.red('Failed to integrate Claude Code workspace commands:'), error.message);
return false;
}
}
/**
* Install Claude Code CLI optimization features
*/
async installClaudeCodeOptimizations(installDir) {
try {
const workspacePath = path.join(installDir, '.workspace');
const optimizationsPath = path.join(workspacePath, 'claude-code-optimizations');
// Ensure optimizations directory exists
if (!fs.existsSync(optimizationsPath)) {
fs.mkdirSync(optimizationsPath, { recursive: true });
}
// Create session manager
const sessionManagerScript = `const ClaudeCodeSessionManager = require('../../tools/installer/lib/claude-code-session-manager');
const ClaudeCodeWorkspaceCommands = require('../../tools/installer/lib/claude-code-workspace-commands');
const ClaudeCodeContextIntegration = require('../../tools/installer/lib/claude-code-context-integration');
const ClaudeCodeMaintenanceSystem = require('../../tools/installer/lib/claude-code-maintenance-system');
const ClaudeCodeUXEnhancements = require('../../tools/installer/lib/claude-code-ux-enhancements');
// Claude Code CLI Enhanced Session Manager
class EnhancedClaudeCodeSession {
constructor(workspaceDir = process.cwd()) {
this.workspaceDir = workspaceDir;
this.sessionManager = new ClaudeCodeSessionManager(workspaceDir);
this.workspaceCommands = new ClaudeCodeWorkspaceCommands(workspaceDir);
this.contextIntegration = new ClaudeCodeContextIntegration(workspaceDir);
this.maintenanceSystem = new ClaudeCodeMaintenanceSystem(workspaceDir);
this.uxEnhancements = new ClaudeCodeUXEnhancements(workspaceDir);
}
async initialize(agentType = 'dev', options = {}) {
console.log('🚀 Starting Claude Code CLI Enhanced Session...');
// Initialize session with automatic features
const sessionResult = await this.sessionManager.initializeSession(agentType, {
name: require('path').basename(this.workspaceDir),
...options
});
if (sessionResult.status === 'initialized') {
// Initialize UX enhancements
await this.uxEnhancements.initializeUXEnhancements(sessionResult.sessionId, agentType);
// Perform startup integrity check
await this.maintenanceSystem.performStartupIntegrityCheck();
// Generate intelligent suggestions
await this.uxEnhancements.generateIntelligentSuggestions();
console.log('✨ Claude Code CLI Enhanced Session ready!');
console.log(' • Native workspace commands active');
console.log(' • Automatic session management enabled');
console.log(' • Context-aware features initialized');
console.log(' • Built-in maintenance system active');
console.log(' • Enhanced UX features enabled');
}
return sessionResult;
}
async executeCommand(commandName, ...args) {
const result = await this.workspaceCommands[commandName]?.(...args);
// Add status indicators to response
return this.uxEnhancements.addWorkspaceStatusIndicators(result, commandName);
}
}
module.exports = EnhancedClaudeCodeSession;
// Auto-initialize if Claude Code CLI session detected
if (process.env.CLAUDE_CODE_SESSION) {
const session = new EnhancedClaudeCodeSession();
session.initialize().catch(console.error);
}
`;
fs.writeFileSync(path.join(optimizationsPath, 'enhanced-session.js'), sessionManagerScript);
// Create workspace command implementations
const workspaceImplementations = `// Claude Code CLI Workspace Command Implementations
// These provide the actual functionality behind the workspace commands in agent definitions
const EnhancedClaudeCodeSession = require('./enhanced-session');
class WorkspaceCommandImplementations {
constructor() {
this.session = new EnhancedClaudeCodeSession();
}
// Implementation for *workspace-init command
async workspaceInit(agentType = 'dev', options = {}) {
return await this.session.initialize(agentType, options);
}
// Implementation for *workspace-status command
async workspaceStatus(detailed = false) {
return await this.session.workspaceCommands.workspaceStatus(detailed);
}
// Implementation for *workspace-cleanup command
async workspaceCleanup(options = {}) {
return await this.session.workspaceCommands.workspaceCleanup(options);
}
// Implementation for *workspace-handoff command
async workspaceHandoff(targetAgent, context = {}) {
return await this.session.workspaceCommands.workspaceHandoff(targetAgent, context);
}
// Implementation for *workspace-sync command
async workspaceSync(options = {}) {
return await this.session.workspaceCommands.workspaceSync(options);
}
}
module.exports = new WorkspaceCommandImplementations();
`;
fs.writeFileSync(path.join(optimizationsPath, 'command-implementations.js'), workspaceImplementations);
// Create configuration file
const optimizationConfig = {
version: '1.0',
created: new Date().toISOString(),
features: {
nativeCommands: true,
automaticSessionManagement: true,
contextAwareHandoffs: true,
builtInMaintenance: true,
enhancedUXFeatures: true
},
settings: {
autoSuggestions: true,
performanceOptimization: true,
intelligentHandoffs: true,
backgroundMaintenance: true
},
integration: {
claudeCodeCLI: true,
workspaceSystem: true,
bmadFramework: true
}
};
fs.writeFileSync(
path.join(optimizationsPath, 'optimization-config.json'),
JSON.stringify(optimizationConfig, null, 2)
);
// Create README for optimizations
const optimizationReadme = `# Claude Code CLI Optimizations
This directory contains enhanced features specifically designed for Claude Code CLI users of the BMAD collaborative workspace system.
## Features
### 🚀 Native Workspace Commands
- \`*workspace-init\` - Initialize collaborative workspace session
- \`*workspace-status\` - Show current workspace status and analytics
- \`*workspace-cleanup\` - Automated maintenance and optimization
- \`*workspace-handoff [agent]\` - Context-aware agent transitions
- \`*workspace-sync\` - Synchronize with latest workspace context
### 🧠 Automatic Session Management
- Automatic session registration and heartbeat monitoring
- Seamless session recovery and context restoration
- Intelligent session cleanup and resource management
### 🔄 Context-Aware Agent Handoffs
- Intelligent handoff opportunity detection
- Enhanced context transfer with smart summarization
- Target-agent specific suggestions and next actions
### 🔧 Built-in Workspace Maintenance
- Automatic integrity checks on session startup
- Proactive issue detection and auto-repair
- Background optimization during idle periods
- Workspace health monitoring and analytics
### ✨ Enhanced User Experience
- Intelligent workspace suggestions based on context
- Productivity analytics and usage insights
- Seamless integration with existing Claude Code CLI workflows
- Context-aware command recommendations
## Usage
These optimizations are automatically enabled when using BMAD agents in Claude Code CLI. No additional configuration required - the enhanced features work transparently with your existing workflow.
## Configuration
Optimization settings can be customized in \`optimization-config.json\`.
`;
fs.writeFileSync(path.join(optimizationsPath, 'README.md'), optimizationReadme);
return true;
} catch (error) {
console.error('Failed to install Claude Code CLI optimizations:', error.message);
return false;
}
}
}
module.exports = WorkspaceSetup;