UNPKG

@sirmrmarty/n8n-nodes-tmux-orchestrator

Version:

n8n nodes for orchestrating Claude AI agents through tmux sessions

586 lines (517 loc) 17.9 kB
import { TmuxBridge } from './tmuxBridge'; import { execSync } from 'child_process'; import * as fs from 'fs'; import { ConditionWaiter } from './conditionWaiter'; import { resourceManager } from './resourceManager'; import { circuitRegistry, CircuitState } from './circuitBreaker'; import { stateManager } from './threadSafeState'; export interface ProjectOrchestratorConfig { checkInterval?: number; // milliseconds maxRetries?: number; autoCreatePR?: boolean; credentials?: any; } export interface ProjectStatus { projectName: string; isComplete: boolean; qaApproved: boolean; readyForPR: boolean; prCreated: boolean; prUrl?: string; lastCheck: Date; retryCount: number; } /** * Autonomous workflow orchestrator for project completion and PR creation */ export class ProjectOrchestrator { private bridge: TmuxBridge; private config: ProjectOrchestratorConfig; private monitoredProjects = stateManager.getMap<string, ProjectStatus>('monitoredProjects'); private monitoringResourceId?: string; private isMonitoring = false; constructor(bridge: TmuxBridge, config: ProjectOrchestratorConfig = {}) { this.bridge = bridge; this.config = { checkInterval: 60000, // 1 minute maxRetries: 5, autoCreatePR: true, ...config, }; } /** * Add project to monitoring queue */ async addProject(projectName: string): Promise<void> { if (!this.monitoredProjects.safeHas(projectName)) { try { await this.monitoredProjects.safeSet(projectName, { projectName, isComplete: false, qaApproved: false, readyForPR: false, prCreated: false, lastCheck: new Date(), retryCount: 0, }); console.log(`Added project ${projectName} to monitoring queue`); // Start monitoring if not already running if (!this.isMonitoring) { this.startMonitoring(); } } catch (error) { console.error(`Failed to add project ${projectName} to monitoring:`, error.message); throw error; } } } /** * Remove project from monitoring */ async removeProject(projectName: string): Promise<void> { try { const deleted = await this.monitoredProjects.safeDelete(projectName); if (deleted) { console.log(`Removed project ${projectName} from monitoring`); } // Stop monitoring if no projects left if (this.monitoredProjects.safeSize() === 0) { await this.stopMonitoring(); } } catch (error) { console.error(`Failed to remove project ${projectName} from monitoring:`, error.message); throw error; } } /** * Start autonomous monitoring */ startMonitoring(): void { if (this.isMonitoring) { return; } console.log('Starting autonomous project monitoring...'); this.isMonitoring = true; const { id } = resourceManager.createInterval(async () => { await this.checkAllProjects(); }, this.config.checkInterval!, 'Project orchestrator monitoring'); this.monitoringResourceId = id; } /** * Stop monitoring */ async stopMonitoring(): Promise<void> { if (this.monitoringResourceId) { await resourceManager.cleanup(this.monitoringResourceId); this.monitoringResourceId = undefined; } this.isMonitoring = false; console.log('Stopped autonomous project monitoring'); } /** * Check all monitored projects for completion */ private async checkAllProjects(): Promise<void> { // Circuit breaker for project monitoring const circuitBreaker = circuitRegistry.getBreaker('project-monitoring', { failureThreshold: 5, recoveryTimeout: 180000, // 3 minutes successThreshold: 3, monitoringWindow: 600000, // 10 minutes maxRetryAttempts: 2, onStateChange: (state, reason) => { console.log(`[Circuit Breaker] Project monitoring: ${state} - ${reason}`); }, onFailure: (error) => { console.error(`[Circuit Breaker] Project monitoring failure:`, error.message); } }); await circuitBreaker.execute(async () => { const promises = this.monitoredProjects.safeKeys().map(async (projectName) => { try { await this.checkProject(projectName); } catch (error) { console.error(`Error checking project ${projectName}:`, error.message); } }); await Promise.allSettled(promises); }, async () => { // Fallback: log warning and skip this monitoring cycle console.warn('[Circuit Breaker] Project monitoring is in open state, skipping monitoring cycle'); // No return value needed for void function }); } /** * Check individual project status and trigger completion workflow */ private async checkProject(projectName: string): Promise<void> { // Circuit breaker for individual project checking const circuitBreaker = circuitRegistry.getBreaker(`project-check-${projectName}`, { failureThreshold: 3, recoveryTimeout: 120000, // 2 minutes successThreshold: 2, maxRetryAttempts: 1, onStateChange: (state, reason) => { console.log(`[Circuit Breaker] Project check for ${projectName}: ${state} - ${reason}`); } }); await circuitBreaker.execute(async () => { return await this.performProjectCheck(projectName); }, async () => { // Fallback: increment retry count and skip this check const status = this.monitoredProjects.safeGet(projectName); if (status) { try { await this.monitoredProjects.safeUpdate(projectName, (currentStatus) => { if (currentStatus) { currentStatus.retryCount++; currentStatus.lastCheck = new Date(); } return currentStatus!; }); console.warn(`[Circuit Breaker] Project check for ${projectName} failed, retry count: ${status.retryCount}`); // Remove from monitoring if too many failures if (status.retryCount >= (this.config.maxRetries || 5)) { console.error(`Max retries reached for ${projectName} - removing from monitoring due to circuit breaker`); await this.removeProject(projectName); } } catch (error) { console.error(`Failed to update status for ${projectName}:`, error.message); } } // No return value needed for void function }); } /** * Perform the actual project check (extracted for circuit breaker protection) */ private async performProjectCheck(projectName: string): Promise<void> { const status = this.monitoredProjects.safeGet(projectName); if (!status || status.prCreated) { return; // Already completed or not found } try { // Check if project session exists const sessions = await this.bridge.getTmuxSessions(); const projectSession = sessions.find(s => s.name === projectName); if (!projectSession) { console.warn(`Project session ${projectName} not found - removing from monitoring`); await this.removeProject(projectName); return; } // Check completion status const completionStatus = await this.checkProjectCompletion(projectName); // Update status thread-safely try { await this.monitoredProjects.safeUpdate(projectName, (currentStatus) => { if (currentStatus) { currentStatus.isComplete = completionStatus.isComplete; currentStatus.qaApproved = completionStatus.qaApproved; currentStatus.readyForPR = completionStatus.readyForPR; currentStatus.lastCheck = new Date(); } return currentStatus!; }); } catch (error) { console.error(`Failed to update project status for ${projectName}:`, error.message); return; } // Get current status for PR checks const currentStatus = this.monitoredProjects.safeGet(projectName); if (!currentStatus) { return; // Status was removed during update } // If ready for PR and auto-create is enabled, create PR if (currentStatus.readyForPR && !currentStatus.prCreated && this.config.autoCreatePR) { console.log(`Project ${projectName} is ready - initiating automatic PR creation...`); const prResult = await this.createAutomaticPR(projectName); if (prResult.success) { try { await this.monitoredProjects.safeUpdate(projectName, (status) => { if (status) { status.prCreated = true; status.prUrl = prResult.prUrl; } return status!; }); console.log(`Autonomous PR creation successful for ${projectName}: ${prResult.prUrl}`); // Notify team await this.notifyProjectCompletion(projectName, prResult.prUrl!); // Remove from monitoring await this.removeProject(projectName); } catch (error) { console.error(`Failed to update PR completion status for ${projectName}:`, error.message); } } else { try { const updatedStatus = await this.monitoredProjects.safeUpdate(projectName, (status) => { if (status) { status.retryCount++; } return status!; }); console.error(`PR creation failed for ${projectName} (attempt ${updatedStatus.retryCount}): ${prResult.error}`); if (updatedStatus.retryCount >= (this.config.maxRetries || 5)) { console.error(`Max retries reached for ${projectName} - removing from monitoring`); await this.removeProject(projectName); } } catch (error) { console.error(`Failed to update retry count for ${projectName}:`, error.message); } } } } catch (error) { try { const updatedStatus = await this.monitoredProjects.safeUpdate(projectName, (status) => { if (status) { status.retryCount++; } return status!; }); console.error(`Error monitoring project ${projectName}:`, error.message); if (updatedStatus && updatedStatus.retryCount >= (this.config.maxRetries || 5)) { console.error(`Max retries reached for ${projectName} - removing from monitoring`); await this.removeProject(projectName); } } catch (updateError) { console.error(`Failed to update error count for ${projectName}:`, updateError.message); } } } /** * Check project completion status */ private async checkProjectCompletion(projectName: string): Promise<{ isComplete: boolean; qaApproved: boolean; readyForPR: boolean; }> { // Circuit breaker for completion checking const circuitBreaker = circuitRegistry.getBreaker(`completion-check-${projectName}`, { failureThreshold: 3, recoveryTimeout: 90000, // 1.5 minutes successThreshold: 2, maxRetryAttempts: 1, onStateChange: (state, reason) => { console.log(`[Circuit Breaker] Completion check for ${projectName}: ${state} - ${reason}`); } }); return await circuitBreaker.execute(async () => { return await this.performCompletionCheck(projectName); }, async () => { // Fallback: return safe status indicating not ready console.warn(`[Circuit Breaker] Completion check for ${projectName} failed, returning safe status`); return { isComplete: false, qaApproved: false, readyForPR: false }; }); } /** * Perform the actual completion check (extracted for circuit breaker protection) */ private async performCompletionCheck(projectName: string): Promise<{ isComplete: boolean; qaApproved: boolean; readyForPR: boolean; }> { // Send completion check request to Claude Code instance await this.bridge.sendClaudeMessage(`${projectName}:0`, 'AUTONOMOUS STATUS CHECK: Please respond with "PROJECT COMPLETE" if all objectives are met and ready for PR. This is an automated check.'); // Wait for response await ConditionWaiter.waitForPrompt(this.bridge, projectName, 0); // Check Claude Code response const output = await this.bridge.captureWindowContent(projectName, 0, 20); let response = ''; if (typeof output === 'string') { response = output.split('\n').slice(-10).join('\n').toLowerCase(); } // Check for completion signals const completionSignals = [ 'project complete', 'ready for pr', 'ready for pull request', 'objectives met', 'deliverables complete', 'implementation finished' ]; const isComplete = completionSignals.some(signal => response.includes(signal)); // Since it's a single Claude Code instance, assume QA is handled internally // Look for quality indicators in the same response const qualityIndicators = [ 'tests passed', 'validation complete', 'quality check', 'all tests', 'qa complete', 'approved' ]; const qaApproved = isComplete && ( qualityIndicators.some(indicator => response.includes(indicator)) || response.includes('project complete') // Implicit approval when project is complete ); return { isComplete, qaApproved, readyForPR: isComplete && qaApproved, }; } /** * Create automatic pull request */ private async createAutomaticPR(projectName: string): Promise<{ success: boolean; prUrl?: string; error?: string; }> { // Circuit breaker for PR creation const circuitBreaker = circuitRegistry.getBreaker(`pr-creation-${projectName}`, { failureThreshold: 2, recoveryTimeout: 300000, // 5 minutes successThreshold: 1, maxRetryAttempts: 1, onStateChange: (state, reason) => { console.log(`[Circuit Breaker] PR creation for ${projectName}: ${state} - ${reason}`); } }); return await circuitBreaker.execute(async () => { return await this.performPRCreation(projectName); }, async () => { // Fallback: return failure status console.warn(`[Circuit Breaker] PR creation for ${projectName} failed, circuit breaker is open`); return { success: false, error: 'PR creation temporarily unavailable due to circuit breaker protection' }; }); } /** * Perform the actual PR creation (extracted for circuit breaker protection) */ private async performPRCreation(projectName: string): Promise<{ success: boolean; prUrl?: string; error?: string; }> { try { // Get project path await this.bridge.sendCommandToWindow(projectName, 0, 'pwd'); await ConditionWaiter.waitForPrompt(this.bridge, projectName, 0); const output = await this.bridge.captureWindowContent(projectName, 0, 5); let projectPath = ''; if (typeof output === 'string') { const lines = output.trim().split('\n'); projectPath = lines[lines.length - 1].trim(); } if (!projectPath) { throw new Error('Could not determine project path'); } // Get current branch const branchCmd = `cd ${projectPath} && git branch --show-current`; await this.bridge.sendCommandToWindow(projectName, 0, branchCmd); await ConditionWaiter.waitForPrompt(this.bridge, projectName, 0); const branchOutput = await this.bridge.captureWindowContent(projectName, 0, 5); let currentBranch = ''; if (typeof branchOutput === 'string') { const lines = branchOutput.trim().split('\n'); currentBranch = lines[lines.length - 1].trim(); } // Push to remote first const pushCmd = `cd ${projectPath} && git add . && git commit -m "Autonomous completion commit" && git push -u origin ${currentBranch}`; await this.bridge.sendCommandToWindow(projectName, 0, pushCmd); await ConditionWaiter.waitForGitOperation(this.bridge, projectName, 0, 'push'); // Create PR using GitHub CLI const prTitle = `[Autonomous] ${projectName} - Project Complete`; const prDescription = this.generatePRDescription(projectName); const prResult = await this.bridge.createGitHubPR(projectPath, { title: prTitle, body: prDescription, base: 'main', head: currentBranch, credentials: this.config.credentials, }); return prResult; } catch (error) { return { success: false, error: error.message, }; } } /** * Generate PR description from template */ private generatePRDescription(projectName: string): string { const template = this.config.credentials?.githubConfig?.prTemplate || ` ## Summary Autonomous completion of ${projectName} project. ## Changes - Project implementation completed autonomously - All objectives met and validated - Quality checks passed ## Test Plan - Automated testing completed - Code quality validation passed - Ready for review and merge ## Quality Status ✅ Quality Approved - All validations passed 🤖 Generated autonomously with Claude Code Tmux Orchestrator `.trim(); return template .replace(/{project_name}/g, projectName) .replace(/{project_description}/g, `Autonomous completion of ${projectName}`) .replace(/{changes_summary}/g, 'Implementation completed autonomously') .replace(/{test_summary}/g, 'All tests passed, quality approved') .replace(/{qa_status}/g, '✅ Quality Approved'); } /** * Notify Claude Code instance of successful completion */ private async notifyProjectCompletion(projectName: string, prUrl: string): Promise<void> { try { const completionMessage = ` 🎉 AUTONOMOUS PROJECT COMPLETION SUCCESS! 🎉 Project: ${projectName} Pull Request: ${prUrl} The project has been completed autonomously and is ready for final review and merge. Next Steps: - Review the pull request - Merge when ready - Deploy to production Autonomous orchestration complete! 🚀 `.trim(); // Notify Claude Code instance await this.bridge.sendClaudeMessage(`${projectName}:0`, completionMessage); console.log(`Claude Code instance notified of autonomous completion for ${projectName}`); } catch (error) { console.error(`Failed to notify Claude Code instance for ${projectName}:`, error.message); } } /** * Get current monitoring status */ getMonitoringStatus(): { isMonitoring: boolean; projectCount: number; projects: ProjectStatus[]; } { return { isMonitoring: this.isMonitoring, projectCount: this.monitoredProjects.safeSize(), projects: this.monitoredProjects.safeValues(), }; } /** * Cleanup resources */ async destroy(): Promise<void> { await this.stopMonitoring(); try { await this.monitoredProjects.safeClear(); } catch (error) { console.error('Failed to clear monitored projects:', error.message); } console.log('Project orchestrator destroyed'); } }