UNPKG

claude-coordination-system

Version:

πŸ€– Multi-Claude Parallel Processing Coordination System - Organize multiple Claude AI instances to work together seamlessly on complex development tasks

909 lines (745 loc) β€’ 26.1 kB
/** * Multi-Claude Worker Core Implementation * Individual worker process that executes tasks with coordination */ const fs = require('fs-extra'); const path = require('path'); const { exec, spawn } = require('child_process'); const { promisify } = require('util'); const chalk = require('chalk'); const EventEmitter = require('events'); const MemoryManager = require('./memory-manager'); const { SecurityManager, SecurityError } = require('./security-manager'); const { logWorker, logError, logPerformance } = require('./development-logger'); const execAsync = promisify(exec); class WorkerCore extends EventEmitter { constructor(workerId, groupId, projectRoot, options = {}) { super(); this.workerId = workerId; this.groupId = groupId; this.projectRoot = projectRoot; this.coordinationDir = path.join(projectRoot, '.claude-coord'); this.stateFile = path.join(this.coordinationDir, 'system-state.json'); this.options = { verbose: false, dryRun: false, maxRetries: 3, taskTimeout: 300000, // 5 minutes per task heartbeatInterval: 15000, // 15 seconds ...options }; this.isRunning = false; this.currentTask = null; this.heartbeatTimer = null; this.acquiredLocks = []; // Initialize memory manager this.memoryManager = new MemoryManager({ maxMemoryMB: options.maxMemoryMB || 256, // 256MB default per worker checkInterval: 30000, // 30s check interval warningThreshold: 0.8, // 80% warning criticalThreshold: 0.9 // 90% critical }); // Initialize security manager this.securityManager = new SecurityManager(projectRoot, { maxPathDepth: 10, allowedCommands: ['npx', 'npm', 'tsc', 'eslint', 'prettier', 'node'], maxCommandLength: 1000, logSecurityEvents: options.verbose || false }); console.log(chalk.blue(`πŸ€– Worker initialized: ${this.workerId} (${this.groupId})`)); } /** * Execute command safely with security validation */ async executeSecureCommand(command, args = [], options = {}) { try { // Validate and sanitize command const sanitized = this.securityManager.sanitizeCommand(command, args); // Execute using spawn for better security control return new Promise((resolve, reject) => { const child = spawn(sanitized.command, sanitized.args, { cwd: options.cwd || this.projectRoot, timeout: options.timeout || this.options.taskTimeout, stdio: ['ignore', 'pipe', 'pipe'], shell: false // Disable shell to prevent injection }); let stdout = ''; let stderr = ''; child.stdout.on('data', (data) => { stdout += data.toString(); }); child.stderr.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { if (code === 0) { resolve({ stdout, stderr, code }); } else { reject(new Error(`Command failed with code ${code}: ${stderr}`)); } }); child.on('error', (error) => { reject(new Error(`Command execution failed: ${error.message}`)); }); // Handle timeout if (options.timeout) { setTimeout(() => { child.kill('SIGTERM'); reject(new Error('Command execution timeout')); }, options.timeout); } }); } catch (error) { if (error instanceof SecurityError) { await logError(this.workerId, 'Security violation in command execution', error); throw error; } throw new Error(`Command execution failed: ${error.message}`); } } /** * Start worker execution */ async start() { if (this.isRunning) { throw new Error('Worker already running'); } this.isRunning = true; try { console.log(chalk.green(`πŸš€ ${this.workerId} starting work on ${this.groupId}`)); await logWorker(this.workerId, 'Starting Worker', { description: `Worker starting with group assignment: ${this.groupId}`, result: 'STARTED', memory: `Memory limit: ${this.options.memoryLimit}MB` }); // Start memory monitoring this.memoryManager.startMonitoring(); // Setup memory event handlers this.setupMemoryHandlers(); // Register with coordinator await this.registerWithCoordinator(); // Start heartbeat this.startHeartbeat(); // Check if in standby mode if (this.groupId === 'STANDBY') { console.log(chalk.yellow(`⏸️ ${this.workerId} waiting in standby mode...`)); await this.updateStatus('standby'); return; // Don't execute tasks, wait for assignment } // Wait for dependencies await this.waitForDependencies(); // Execute tasks await this.executeTasks(); // Mark as completed and switch to inactive/standby mode await this.updateStatus('inactive'); console.log(chalk.green(`πŸŽ‰ ${this.workerId} completed all tasks`)); console.log(chalk.yellow(`⏸️ ${this.workerId} switching to inactive mode, waiting for new assignments...`)); await logWorker(this.workerId, 'Tasks Completed', { description: `All tasks completed, switching to inactive mode`, result: 'SUCCESS - INACTIVE', notes: `Worker remains active for reassignment` }); // Stay alive and wait for reassignment instead of exiting return await this.waitForReassignment(); } catch (error) { console.error(chalk.red(`❌ ${this.workerId} failed:`), error.message); await this.updateStatus('error', { error: error.message }); throw error; } finally { await this.cleanup(); } } /** * Register worker with coordination system */ async registerWithCoordinator() { const state = await this.loadSystemState(); // Get group configuration const groupConfig = state.dependencies[this.groupId]; if (!groupConfig) { throw new Error(`Group configuration not found: ${this.groupId}`); } const workerInfo = { group: this.groupId, status: 'initializing', started_at: new Date().toISOString(), last_heartbeat: new Date().toISOString(), current_files: [], progress: { total_tasks: this.getTaskCount(groupConfig), completed_tasks: 0, current_task: null } }; state.active_workers[this.workerId] = workerInfo; state.task_progress.active_groups++; await this.saveSystemState(state); console.log(chalk.green(`βœ… ${this.workerId} registered with coordinator`)); this.emit('registered'); } /** * Update worker status in coordination system */ async updateStatus(status, additionalData = {}) { const state = await this.loadSystemState(); if (state.active_workers[this.workerId]) { Object.assign(state.active_workers[this.workerId], { status, last_heartbeat: new Date().toISOString(), ...additionalData }); await this.saveSystemState(state); } this.emit('status_updated', { status, ...additionalData }); } /** * Wait for dependencies to complete */ async waitForDependencies() { console.log(chalk.blue(`⏳ ${this.workerId} checking dependencies...`)); while (true) { const dependencyCheck = await this.checkDependencies(); if (dependencyCheck.canProceed) { console.log(chalk.green(`βœ… ${this.workerId} dependencies satisfied`)); break; } console.log(chalk.yellow( `⏳ ${this.workerId} waiting for: ${dependencyCheck.blockers.join(', ')}` )); await this.sleep(10000); // Wait 10 seconds } } /** * Check if dependencies are satisfied */ async checkDependencies() { const state = await this.loadSystemState(); const groupConfig = state.dependencies[this.groupId]; if (!groupConfig || !groupConfig.blocked_by) { return { canProceed: true, blockers: [] }; } const blockers = []; for (const blockingGroup of groupConfig.blocked_by) { const blockingWorker = Object.values(state.active_workers).find( w => w.group === blockingGroup ); if (!blockingWorker || blockingWorker.status !== 'completed') { blockers.push(blockingGroup); } } return { canProceed: blockers.length === 0, blockers }; } /** * Execute all tasks for this worker group */ async executeTasks() { const state = await this.loadSystemState(); const groupConfig = state.dependencies[this.groupId]; console.log(chalk.blue(`πŸ”§ ${this.workerId} starting task execution`)); await this.updateStatus('working'); const tasks = this.generateTasks(groupConfig); for (let i = 0; i < tasks.length; i++) { const task = tasks[i]; if (!this.isRunning) { console.log(chalk.yellow(`πŸ›‘ ${this.workerId} stopping execution`)); break; } this.currentTask = task; try { await this.executeTask(task); // Update progress const currentState = await this.loadSystemState(); if (currentState.active_workers[this.workerId]) { currentState.active_workers[this.workerId].progress.completed_tasks = i + 1; await this.saveSystemState(currentState); } } catch (error) { console.error(chalk.red(`❌ Task failed: ${task.name}`), error.message); if (task.retries < this.options.maxRetries) { console.log(chalk.yellow(`πŸ”„ Retrying task: ${task.name}`)); task.retries++; i--; // Retry the same task } else { throw error; } } // Brief pause between tasks await this.sleep(1000); } } /** * Execute a single task */ async executeTask(task) { console.log(chalk.blue(`πŸ“ ${this.workerId} executing: ${task.name}`)); // Update current task await this.updateStatus('working', { current_files: task.files, progress: { current_task: task.name } }); // Acquire file locks await this.acquireFileLocks(task.files); try { // Execute task based on type switch (task.action) { case 'typescript_fix': await this.executeTypeScriptFix(task); break; case 'eslint_fix': await this.executeESLintFix(task); break; case 'bundle_analysis': await this.executeBundleAnalysis(task); break; case 'dependency_cleanup': await this.executeDependencyCleanup(task); break; case 'custom': if (typeof task.handler === 'function') { await task.handler(task, this); } break; default: console.log(chalk.yellow(`⚠️ Unknown task action: ${task.action}`)); } console.log(chalk.green(`βœ… ${this.workerId} completed: ${task.name}`)); } finally { // Always release file locks await this.releaseFileLocks(task.files); } } /** * File locking system */ async acquireFileLocks(files) { const state = await this.loadSystemState(); for (const file of files) { // Check if file is locked by another worker if (state.file_locks[file] && state.file_locks[file] !== this.workerId) { // Wait for file to become available await this.waitForFileAvailability(file); } // Acquire lock state.file_locks[file] = this.workerId; this.acquiredLocks.push(file); } await this.saveSystemState(state); if (this.options.verbose) { console.log(chalk.gray(`πŸ”’ ${this.workerId} acquired locks: ${files.join(', ')}`)); } } async releaseFileLocks(files) { const state = await this.loadSystemState(); for (const file of files) { if (state.file_locks[file] === this.workerId) { delete state.file_locks[file]; this.acquiredLocks = this.acquiredLocks.filter(f => f !== file); } } await this.saveSystemState(state); if (this.options.verbose && files.length > 0) { console.log(chalk.gray(`πŸ”“ ${this.workerId} released locks: ${files.join(', ')}`)); } } async waitForFileAvailability(file, maxWaitTime = 300000) { const startTime = Date.now(); while (Date.now() - startTime < maxWaitTime) { const state = await this.loadSystemState(); if (!state.file_locks[file] || state.file_locks[file] === this.workerId) { return true; } console.log(chalk.yellow( `⏳ ${this.workerId} waiting for ${file} (locked by ${state.file_locks[file]})` )); await this.sleep(5000); } throw new Error(`Timeout waiting for file: ${file}`); } /** * Task implementations */ async executeTypeScriptFix(task) { for (const file of task.files) { if (this.options.dryRun) { console.log(chalk.blue(`[DRY RUN] Would fix TypeScript errors in: ${file}`)); await this.sleep(1000); continue; } try { // Validate file path for security const safePath = this.securityManager.sanitizePath(file); // Check for TypeScript errors using secure command execution const { stderr } = await this.executeSecureCommand('npx', [ 'tsc', '--noEmit', path.relative(this.projectRoot, safePath) ], { cwd: this.projectRoot, timeout: this.options.taskTimeout }); if (stderr.includes('error TS')) { console.log(chalk.yellow(`πŸ”§ Found TypeScript errors in ${file}`)); // Here would be actual TypeScript fixing logic await this.sleep(2000); // Simulate fix time } } catch (error) { // TypeScript errors expected - continue with fixes console.log(chalk.blue(`πŸ”§ Processing TypeScript fixes for ${file}`)); await this.sleep(2000); } } } async executeESLintFix(task) { for (const file of task.files) { if (this.options.dryRun) { console.log(chalk.blue(`[DRY RUN] Would analyze ESLint issues in: ${file}`)); await this.sleep(1000); continue; } try { const { stdout } = await execAsync( `npx eslint ${file} --format=json || echo "[]"`, { cwd: this.projectRoot } ); const results = JSON.parse(stdout || '[]'); if (results.length > 0 && results[0].messages?.length > 0) { console.log(chalk.yellow( `πŸ“ Found ${results[0].messages.length} ESLint issues in ${file}` )); // Manual analysis - no auto-fix per Zero Protocol } } catch (error) { console.log(chalk.yellow(`⚠️ ESLint analysis warning for ${file}: ${error.message}`)); } await this.sleep(1500); } } async executeBundleAnalysis(task) { console.log(chalk.blue('πŸ“Š Analyzing bundle size and dependencies')); if (!this.options.dryRun) { // Actual bundle analysis would go here await this.sleep(3000); } console.log(chalk.green('βœ… Bundle analysis completed')); } async executeDependencyCleanup(task) { console.log(chalk.blue('🧹 Analyzing unused dependencies')); if (!this.options.dryRun) { // Dependency cleanup logic would go here await this.sleep(2000); } console.log(chalk.green('βœ… Dependency cleanup completed')); } /** * Generate tasks for the worker group */ generateTasks(groupConfig) { const baseTaskMap = { GRUP1_TYPESCRIPT: [ { name: 'Fix TypeScript syntax errors', files: groupConfig.files.filter(f => f.includes('.ts')), action: 'typescript_fix' }, { name: 'Validate build process', files: ['tsconfig.json'], action: 'typescript_fix' } ], GRUP2_ESLINT: [ { name: 'Analyze ESLint warnings', files: groupConfig.files, action: 'eslint_fix' } ], GRUP3_BUNDLE: [ { name: 'Analyze bundle size', files: ['package.json', 'next.config.*'], action: 'bundle_analysis' }, { name: 'Clean unused dependencies', files: ['package.json', 'package-lock.json'], action: 'dependency_cleanup' } ] }; const tasks = baseTaskMap[this.groupId] || [ { name: `Process ${this.groupId} tasks`, files: groupConfig.files, action: 'custom' } ]; // Add retry counter to each task return tasks.map(task => ({ ...task, retries: 0 })); } getTaskCount(groupConfig) { const tasks = this.generateTasks(groupConfig); return tasks.length; } /** * Heartbeat system */ startHeartbeat() { this.heartbeatTimer = setInterval(async () => { try { await this.sendHeartbeat(); } catch (error) { console.error(chalk.red(`❌ Heartbeat failed: ${error.message}`)); } }, this.options.heartbeatInterval); if (this.options.verbose) { console.log(chalk.gray(`πŸ’“ Heartbeat started (${this.options.heartbeatInterval}ms)`)); } } async sendHeartbeat() { await this.updateStatus(this.currentTask ? 'working' : 'idle'); } /** * System state management */ async loadSystemState() { try { const data = await fs.readFile(this.stateFile, 'utf8'); return JSON.parse(data); } catch (error) { throw new Error(`Failed to load system state: ${error.message}`); } } async saveSystemState(state) { try { await fs.writeFile(this.stateFile, JSON.stringify(state, null, 2)); } catch (error) { throw new Error(`Failed to save system state: ${error.message}`); } } /** * Setup memory management event handlers */ setupMemoryHandlers() { this.memoryManager.on('memoryWarning', (stats) => { console.log(chalk.yellow(`⚠️ ${this.workerId} memory warning: ${stats.current}MB/${stats.max}MB`)); }); this.memoryManager.on('memoryCritical', (stats) => { console.log(chalk.red(`🚨 ${this.workerId} critical memory usage: ${stats.current}MB/${stats.max}MB`)); console.log(chalk.red('πŸ”„ Consider restarting this worker')); }); this.memoryManager.on('criticalError', (error) => { console.error(chalk.red(`πŸ’₯ ${this.workerId} critical error: ${error.message}`)); this.shutdown(); }); } /** * Get memory statistics */ getMemoryStats() { return this.memoryManager.getStats(); } /** * Get comprehensive worker status for user queries */ async getWorkerStatus() { const memStats = this.getMemoryStats(); const systemState = await this.loadSystemState(); const otherWorkers = Object.entries(systemState.active_workers || {}) .filter(([id]) => id !== this.workerId) .map(([id, worker]) => ({ id, group: worker.group, status: worker.status, memory: worker.memory, lastSeen: worker.last_heartbeat ? this.formatTimeAgo(new Date(worker.last_heartbeat)) : 'Unknown' })); return { // Current worker info self: { workerId: this.workerId, groupId: this.groupId, status: this.isRunning ? 'working' : 'stopped', currentTask: this.currentTask, memory: { current: memStats.current, limit: memStats.limit, usage: memStats.usage }, lockedFiles: [...this.acquiredLocks] }, // System overview system: { totalWorkers: Object.keys(systemState.active_workers || {}).length, maxWorkers: systemState.system_info?.max_workers || 6, totalFileLocks: Object.keys(systemState.file_locks || {}).length, healthy: systemState.system_info?.healthy !== false }, // Other workers otherWorkers, // Dependencies dependencies: systemState.dependencies || {}, // Recent activity (if available) recentActivity: systemState.recent_activity || [] }; } /** * Format user-friendly status response */ async getStatusResponse() { const status = await this.getWorkerStatus(); let response = `πŸ€– Current Status:\n`; response += `Worker ID: ${status.self.workerId}\n`; response += `Group: ${status.self.groupId}\n`; if (status.self.currentTask) { response += `Task: ${status.self.currentTask}\n`; } response += `Memory: ${status.self.memory.current}MB / ${status.self.memory.limit}MB (${status.self.memory.usage}%)\n`; if (status.self.lockedFiles.length > 0) { response += `Locked Files: ${status.self.lockedFiles.join(', ')}\n`; } response += `\nπŸ”„ Coordination Status:\n`; if (status.otherWorkers.length > 0) { status.otherWorkers.forEach(worker => { const memInfo = worker.memory ? ` - Memory ${worker.memory.usage}%` : ''; response += `- ${worker.id} (${worker.group}): ${worker.status}${memInfo}\n`; }); } else { response += `- No other workers active\n`; } response += `System ${status.system.healthy ? 'healthy βœ…' : 'unhealthy ❌'}`; return response; } /** * Format time ago helper */ formatTimeAgo(date) { const now = new Date(); const diffMs = now - date; const diffSecs = Math.floor(diffMs / 1000); const diffMins = Math.floor(diffSecs / 60); if (diffSecs < 60) return `${diffSecs}s ago`; if (diffMins < 60) return `${diffMins}m ago`; return date.toLocaleTimeString(); } /** * Cleanup and shutdown */ async cleanup() { console.log(chalk.blue(`🧹 ${this.workerId} cleaning up...`)); // Stop memory monitoring if (this.memoryManager) { await this.memoryManager.gracefulShutdown(); } // Stop heartbeat if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } // Release all file locks if (this.acquiredLocks.length > 0) { await this.releaseFileLocks([...this.acquiredLocks]); } // Update final status if (this.isRunning) { await this.updateStatus('stopped'); } this.isRunning = false; } /** * Assign worker to a specific group (for standby mode) */ async assignToGroup(groupId) { if (this.groupId !== 'STANDBY') { throw new Error('Worker must be in STANDBY mode to assign to group'); } console.log(chalk.blue(`πŸ”„ Switching from STANDBY to ${groupId}`)); this.groupId = groupId; // Update status await this.updateStatus('initializing'); // Restart execution with new group this.executeTasks().then(() => { console.log(chalk.green(`πŸŽ‰ ${this.workerId} completed all tasks for ${groupId}`)); }).catch((error) => { console.error(chalk.red(`❌ ${this.workerId} failed:`, error.message)); }); return true; } /** * Pause worker operations (used when coordinator disconnects) */ async pause() { if (!this.isRunning) return; console.log(chalk.yellow(`⏸️ ${this.workerId} pausing operations`)); this.isRunning = false; this.currentTask = null; // Stop heartbeat if (this.heartbeatTimer) { clearTimeout(this.heartbeatTimer); this.heartbeatTimer = null; } await this.updateStatus('paused'); this.emit('paused'); } /** * Resume worker operations (used when coordinator reconnects) */ async resume() { if (this.isRunning) return; console.log(chalk.green(`▢️ ${this.workerId} resuming operations`)); this.isRunning = true; await this.updateStatus('resuming'); this.emit('resumed'); // Restart execution this.executeTasks().then(() => { console.log(chalk.green(`πŸŽ‰ ${this.workerId} completed all tasks for ${this.groupId}`)); }).catch((error) => { console.error(chalk.red(`❌ ${this.workerId} failed:`, error.message)); }); } async shutdown() { console.log(chalk.yellow(`πŸ›‘ ${this.workerId} shutting down...`)); this.isRunning = false; await this.cleanup(); this.emit('shutdown'); } /** * Wait for reassignment while in inactive mode */ async waitForReassignment() { console.log(chalk.gray(`⏳ ${this.workerId} waiting for new assignments...`)); // Check for reassignment every 10 seconds const reassignmentInterval = setInterval(async () => { try { const state = await this.loadCoordinatorState(); const worker = state.active_workers[this.workerId]; if (worker && worker.status === 'reassigned') { console.log(chalk.blue(`πŸ”„ ${this.workerId} received new assignment: ${worker.group}`)); clearInterval(reassignmentInterval); // Update group and restart this.groupId = worker.group; await this.updateStatus('initializing'); return await this.executeTasks(); } } catch (error) { // Silently handle errors during reassignment check } }, 10000); // Keep the worker alive return new Promise(() => { // This promise never resolves, keeping worker alive }); } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } module.exports = WorkerCore;