UNPKG

@sirmrmarty/n8n-nodes-tmux-orchestrator

Version:

n8n nodes for orchestrating Claude AI agents through tmux sessions

1,149 lines (1,146 loc) • 64.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.TmuxBridge = void 0; const child_process_1 = require("child_process"); const path = __importStar(require("path")); const fs_1 = require("fs"); const crypto_1 = require("crypto"); const paths_1 = require("./paths"); const secureExecution_1 = require("./secureExecution"); const cryptoQA_1 = require("./cryptoQA"); const qaEvents_1 = require("./qaEvents"); const performanceOptimizations_1 = require("./performanceOptimizations"); const pythonProcessPool_1 = require("./pythonProcessPool"); const secureTempFile_1 = require("./secureTempFile"); const conditionWaiter_1 = require("./conditionWaiter"); const circuitBreaker_1 = require("./circuitBreaker"); class TmuxBridge { constructor(config) { this.approvalCache = new Map(); this.blockCache = new Map(); this.config = config || {}; this.pathResolver = new paths_1.PathResolver(); this.workflowOrchestrator = qaEvents_1.qaEventBus.createWorkflowOrchestrator(); this.sessionsCache = new performanceOptimizations_1.PerformanceCache({ maxSize: 100, maxAge: 60000, }); this.windowContentCache = new performanceOptimizations_1.PerformanceCache({ maxSize: 500, maxAge: 30000, }); this.performanceMonitor = new performanceOptimizations_1.PerformanceMonitor(); this.debouncedCapture = (0, performanceOptimizations_1.debounce)(this.captureWindowContentDirect.bind(this), 100); this.throttledStatus = (0, performanceOptimizations_1.throttle)(this.getAllWindowsStatusDirect.bind(this), 1000); try { this.validateDependencies(); this.pythonScriptPath = this.pathResolver.getScriptPathSync('tmux_wrapper.py', config?.externalScriptsDir); } catch (error) { console.warn('TmuxBridge initialization warning:', error.message); this.pythonScriptPath = path.join(__dirname, 'tmux_wrapper.py'); } this.poolConfig = { maxProcesses: 6, minProcesses: 2, idleTimeout: 60000, requestTimeout: 30000, maxErrorCount: 5, healthCheckInterval: 30000, processRespawnDelay: 1000, }; try { this.pythonPool = new pythonProcessPool_1.PythonProcessPool(this.pythonScriptPath, this.poolConfig); } catch (error) { console.warn('Python process pool initialization failed:', error.message); this.pythonPool = null; } } async initializeClaudeAgent(sessionName, windowIndex, credentials = {}) { let claudeCommand = 'claude'; let subagentConfig = ''; if (credentials?.subagentConfig) { const subagentCfg = credentials.subagentConfig; if (subagentCfg.enableAllSubagents) { subagentConfig = '--subagents all'; } else if (subagentCfg.customSubagents) { subagentConfig = `--subagents ${subagentCfg.customSubagents}`; } } if (credentials?.claudeCommandOptions) { claudeCommand += ` ${credentials.claudeCommandOptions}`; } if (credentials?.claudeCommand) { claudeCommand = credentials.claudeCommand; } const fullCommand = `${claudeCommand} ${subagentConfig}`.trim(); const targetWindow = `${sessionName}:${windowIndex}`; await (0, secureExecution_1.secureTmux)('send-keys', ['-t', targetWindow, `"${fullCommand}"`, 'Enter']); await conditionWaiter_1.ConditionWaiter.waitForClaudeReady(this, sessionName, windowIndex); try { const output = await this.captureWindowContent(sessionName, windowIndex, 10); if (typeof output === 'string' && !output.includes('Claude')) { console.warn(`Claude agent may not have started properly in ${targetWindow}`); } } catch (error) { console.warn(`Failed to verify Claude agent startup: ${error.message}`); } } async setupGitHubCLI(credentials) { try { if (!credentials?.githubConfig?.githubToken) { console.warn('No GitHub token provided - GitHub CLI operations may fail'); return false; } const token = credentials.githubConfig.githubToken; await (0, secureExecution_1.secureExec)({ command: 'gh', args: ['auth', 'login', '--with-token'], timeout: 10000, }); const result = await (0, secureExecution_1.secureExec)({ command: 'gh', args: ['auth', 'status'] }); return result.success; } catch (error) { console.error('Failed to setup GitHub CLI:', error.message); return false; } } async createGitHubPR(projectPath, options) { try { if (options.credentials) { const authSuccess = await this.setupGitHubCLI(options.credentials); if (!authSuccess) { return { success: false, error: 'GitHub authentication failed' }; } } const result = await (0, secureExecution_1.secureExec)({ command: 'gh', args: [ 'pr', 'create', '--title', options.title, '--body', options.body, '--base', options.base, '--head', options.head, '--repo', options.credentials?.githubConfig?.defaultRepository || 'origin' ], cwd: projectPath, timeout: 30000, }); if (result.success && result.stdout) { const urlMatch = result.stdout.match(/https:\/\/github\.com\/[^\s]+\/pull\/\d+/); const prUrl = urlMatch ? urlMatch[0] : ''; return { success: true, prUrl, }; } return { success: false, error: result.stderr || 'PR creation failed' }; } catch (error) { return { success: false, error: `GitHub PR creation error: ${error.message}` }; } } async isGitHubCLIReady() { try { const result = await (0, secureExecution_1.secureExec)({ command: 'gh', args: ['--version'] }); if (!result.success) { return false; } const authResult = await (0, secureExecution_1.secureExec)({ command: 'gh', args: ['auth', 'status'] }); return authResult.success; } catch { return false; } } async executePython(method, args = []) { const startTime = Date.now(); try { if (!this.pythonPool) { console.warn('Python process pool not available, falling back to direct execution'); return this.executePythonDirect(method, args); } const result = await this.pythonPool.execute(method, args); const duration = Date.now() - startTime; this.performanceMonitor.recordExecutionTime(duration); return result; } catch (error) { const duration = Date.now() - startTime; this.performanceMonitor.recordExecutionTime(duration); console.warn('Python pool execution failed, falling back to direct execution:', error.message); return this.executePythonDirect(method, args); } } async executePythonDirect(method, args = []) { return new Promise((resolve, reject) => { const pythonArgs = [this.pythonScriptPath, method, ...args.map(arg => JSON.stringify(arg))]; const python = (0, child_process_1.spawn)('python3', pythonArgs); let stdout = ''; let stderr = ''; python.stdout.on('data', (data) => { stdout += data.toString(); }); python.stderr.on('data', (data) => { stderr += data.toString(); }); python.on('close', (code) => { if (code !== 0) { reject(new Error(`Python script failed: ${stderr}`)); } else { try { const result = JSON.parse(stdout); resolve(result); } catch (error) { resolve(stdout); } } }); }); } async getTmuxSessions() { const circuitBreaker = circuitBreaker_1.circuitRegistry.getBreaker('tmux-session-list', { failureThreshold: 3, recoveryTimeout: 60000, successThreshold: 2, monitoringWindow: 300000, maxRetryAttempts: 2, onStateChange: (state, reason) => { console.log(`[Circuit Breaker] Tmux session list: ${state} - ${reason}`); }, onFailure: (error) => { console.error(`[Circuit Breaker] Tmux session list failure:`, error.message); } }); return await circuitBreaker.execute(async () => { const startTime = Date.now(); const cacheKey = 'tmux_sessions'; try { const cached = this.sessionsCache.get(cacheKey); if (cached) { const duration = Date.now() - startTime; this.performanceMonitor.recordExecutionTime(duration); return Array.isArray(cached) ? cached : []; } const result = await this.executePython('get_tmux_sessions'); const sessionsArray = Array.isArray(result) ? result : []; this.sessionsCache.set(cacheKey, sessionsArray); const duration = Date.now() - startTime; this.performanceMonitor.recordExecutionTime(duration); const cacheMetrics = this.sessionsCache.getMetrics(); this.performanceMonitor.updateCacheMetrics(cacheMetrics.hits, cacheMetrics.misses); return sessionsArray; } catch (error) { console.error('Error getting tmux sessions:', error); const duration = Date.now() - startTime; this.performanceMonitor.recordExecutionTime(duration); return []; } }, async () => { console.warn('[Circuit Breaker] Tmux session listing is temporarily unavailable, returning empty list'); return []; }); } async captureWindowContent(sessionName, windowIndex, numLines = 50) { const circuitBreaker = circuitBreaker_1.circuitRegistry.getBreaker('tmux-content-capture', { failureThreshold: 4, recoveryTimeout: 45000, successThreshold: 2, monitoringWindow: 240000, maxRetryAttempts: 1, onStateChange: (state, reason) => { console.log(`[Circuit Breaker] Tmux content capture: ${state} - ${reason}`); }, onFailure: (error) => { console.error(`[Circuit Breaker] Tmux content capture failure:`, error.message); } }); return await circuitBreaker.execute(async () => { const startTime = Date.now(); const cacheKey = `window_content_${sessionName}_${windowIndex}_${numLines}`; try { const cached = this.windowContentCache.get(cacheKey); if (cached) { const duration = Date.now() - startTime; this.performanceMonitor.recordExecutionTime(duration); return cached; } const result = await this.debouncedCapture(sessionName, windowIndex, numLines); this.windowContentCache.set(cacheKey, result, 30000); const duration = Date.now() - startTime; this.performanceMonitor.recordExecutionTime(duration); return result; } catch (error) { const duration = Date.now() - startTime; this.performanceMonitor.recordExecutionTime(duration); throw new Error(`Failed to capture window content: ${error.message}`); } }, async () => { console.warn(`[Circuit Breaker] Tmux content capture is temporarily unavailable for ${sessionName}:${windowIndex}`); return '[Content capture temporarily unavailable due to circuit breaker protection]'; }); } async captureWindowContentDirect(sessionName, windowIndex, numLines = 50) { try { const result = await this.executePython('capture_window_content', [sessionName, windowIndex, numLines]); if (typeof result === 'string') { return result; } else if (result === null || result === undefined) { return ''; } else if (typeof result === 'object') { try { return JSON.stringify(result, null, 2); } catch (stringifyError) { return String(result); } } else { return String(result); } } catch (error) { throw new Error(`Failed to capture window content: ${error.message}`); } } async getWindowInfo(sessionName, windowIndex) { try { const result = await this.executePython('get_window_info', [sessionName, windowIndex]); return result; } catch (error) { throw new Error(`Failed to get window info: ${error.message}`); } } async sendKeysToWindow(sessionName, windowIndex, keys) { try { const result = await this.executePython('send_keys_to_window', [sessionName, windowIndex, keys, false]); return result === true; } catch (error) { throw new Error(`Failed to send keys: ${error.message}`); } } async sendCommandToWindow(sessionName, windowIndex, command) { const circuitBreaker = circuitBreaker_1.circuitRegistry.getBreaker('tmux-command-send', { failureThreshold: 5, recoveryTimeout: 30000, successThreshold: 3, monitoringWindow: 180000, maxRetryAttempts: 2, onStateChange: (state, reason) => { console.log(`[Circuit Breaker] Tmux command send: ${state} - ${reason}`); }, onFailure: (error) => { console.error(`[Circuit Breaker] Tmux command send failure:`, error.message); } }); return await circuitBreaker.execute(async () => { try { const result = await this.executePython('send_command_to_window', [sessionName, windowIndex, command, false]); return result === true; } catch (error) { throw new Error(`Failed to send command: ${error.message}`); } }, async () => { console.warn(`[Circuit Breaker] Tmux command sending is temporarily unavailable for ${sessionName}:${windowIndex}`); return false; }); } async getAllWindowsStatus() { const startTime = Date.now(); try { const result = await this.throttledStatus(); const duration = Date.now() - startTime; this.performanceMonitor.recordExecutionTime(duration); return result; } catch (error) { const duration = Date.now() - startTime; this.performanceMonitor.recordExecutionTime(duration); throw new Error(`Failed to get windows status: ${error.message}`); } } async getAllWindowsStatusDirect() { try { const result = await this.executePython('get_all_windows_status'); if (result && result.sessions) { const enhancedResult = await this.analyzeTeamCoordination(result); return enhancedResult; } return result; } catch (error) { throw new Error(`Failed to get windows status: ${error.message}`); } } async analyzeTeamCoordination(rawStatus) { try { const analysis = { ...rawStatus, coordinationAnalysis: { timestamp: new Date().toISOString(), teamHealth: 'unknown', communicationIssues: [], recommendations: [], blockers: [] } }; for (const session of rawStatus.sessions || []) { const projectAnalysis = this.analyzeProjectSession(session); session.projectHealth = projectAnalysis.health; session.teamCoordination = projectAnalysis.coordination; session.qaStatus = projectAnalysis.qaStatus; session.blockers = projectAnalysis.blockers; analysis.coordinationAnalysis.communicationIssues.push(...projectAnalysis.issues); analysis.coordinationAnalysis.recommendations.push(...projectAnalysis.recommendations); analysis.coordinationAnalysis.blockers.push(...projectAnalysis.blockers); } analysis.coordinationAnalysis.teamHealth = this.calculateOverallTeamHealth(rawStatus.sessions || []); return analysis; } catch (error) { console.warn('Failed to analyze team coordination:', error.message); return rawStatus; } } analyzeProjectSession(session) { const analysis = { health: 'unknown', coordination: 'unknown', qaStatus: 'unknown', issues: [], recommendations: [], blockers: [] }; const windows = session.windows || []; const pmWindow = windows.find((w) => w.index === 0); const qaWindow = windows.find((w) => w.index === 1); const devWindows = windows.filter((w) => w.index > 1); if (pmWindow?.info?.content) { const pmContent = pmWindow.info.content.toLowerCase(); if (pmContent.includes('stalled') || pmContent.includes('blocked')) { analysis.issues.push('Project Manager reporting stalled status'); analysis.blockers.push('PM coordination issues'); } if (pmContent.includes('repeated status') || pmContent.includes('same as last')) { analysis.issues.push('Project Manager stuck in status loop'); analysis.recommendations.push('PM needs fresh task assignment or unblocking'); } } if (qaWindow?.info?.content) { const qaContent = qaWindow.info.content.toLowerCase(); if (qaContent.includes('awaiting') || qaContent.includes('idle')) { analysis.qaStatus = 'ready'; } else if (qaContent.includes('testing') || qaContent.includes('running')) { analysis.qaStatus = 'active'; } else if (qaContent.includes('blocked') || qaContent.includes('error')) { analysis.qaStatus = 'blocked'; analysis.blockers.push('QA system issues'); } } for (const devWindow of devWindows) { if (devWindow?.info?.content) { const devContent = devWindow.info.content; if (devContent.includes('command not found')) { analysis.issues.push(`Developer window ${devWindow.index}: Command recognition issues`); analysis.recommendations.push('Initialize agent shells with proper command handling'); } } } if (analysis.qaStatus === 'ready' && analysis.issues.some(i => i.includes('PM'))) { analysis.coordination = 'miscommunication'; analysis.recommendations.push('PM should coordinate with ready QA team'); } else if (analysis.issues.length === 0) { analysis.coordination = 'good'; } else { analysis.coordination = 'needs-attention'; } if (analysis.blockers.length > 0) { analysis.health = 'critical'; } else if (analysis.issues.length > 2) { analysis.health = 'poor'; } else if (analysis.issues.length > 0) { analysis.health = 'fair'; } else { analysis.health = 'good'; } return analysis; } calculateOverallTeamHealth(sessions) { if (sessions.length === 0) return 'unknown'; const healthScores = sessions.map(session => { switch (session.projectHealth) { case 'good': return 4; case 'fair': return 3; case 'poor': return 2; case 'critical': return 1; default: return 0; } }); const avgScore = healthScores.reduce((a, b) => a + b, 0) / healthScores.length; if (avgScore >= 3.5) return 'excellent'; if (avgScore >= 2.5) return 'good'; if (avgScore >= 1.5) return 'fair'; if (avgScore >= 0.5) return 'poor'; return 'critical'; } async findWindowByName(windowName) { try { const result = await this.executePython('find_window_by_name', [windowName]); return result; } catch (error) { throw new Error(`Failed to find window: ${error.message}`); } } async createMonitoringSnapshot() { try { const result = await this.executePython('create_monitoring_snapshot'); return result; } catch (error) { throw new Error(`Failed to create monitoring snapshot: ${error.message}`); } } async sendClaudeMessage(target, message) { try { const scriptPath = await this.pathResolver.getScriptPath('send-claude-message.sh', this.config.externalScriptsDir); if (!target || typeof target !== 'string') { throw new Error('Invalid target parameter'); } if (!message || typeof message !== 'string') { throw new Error('Invalid message parameter'); } if (message.length > 10000) { throw new Error('Message too long (max 10000 characters)'); } const sanitizedMessage = message .replace(/[\\"]/g, '\\$&') .replace(/\n/g, '\\n') .replace(/\r/g, '\\r') .replace(/\t/g, '\\t') .replace(/\0/g, ''); const fullCommand = `"${scriptPath}" "${target}" "${sanitizedMessage}"`; const usesTempFile = fullCommand.length > 800; let result; let tempFile = ''; if (usesTempFile) { let tempFileInfo; try { tempFileInfo = await secureTempFile_1.SecureTempFileManager.createSecureTempFile(message, { prefix: 'claude_message', suffix: 'txt', maxAge: 5 * 60 * 1000 }); tempFile = tempFileInfo.path; result = await (0, secureExecution_1.secureExec)({ command: 'bash', args: ['-c', `"${scriptPath}" "${target}" --file "${tempFile}"`], timeout: 10000 }); } finally { if (tempFileInfo) { try { await tempFileInfo.cleanup(); } catch (cleanupError) { console.warn(`Failed to cleanup secure temp file: ${cleanupError.message}`); } } } } else { result = await (0, secureExecution_1.secureExec)({ command: 'bash', args: ['-c', `"${scriptPath}" "${target}" "${sanitizedMessage}"`], timeout: 10000 }); } if (!result.success) { throw new Error(`Script execution failed: ${result.stderr}`); } return true; } catch (error) { throw new Error(`Failed to send Claude message: ${error.message}`); } } async suggestSubagent(target, agentType, context) { try { const allowedAgentTypes = ['pm', 'developer', 'engineer', 'general']; if (!allowedAgentTypes.includes(agentType)) { throw new Error(`Invalid agent type: ${agentType}`); } if (!target || typeof target !== 'string') { throw new Error('Invalid target parameter'); } const scriptPath = await this.pathResolver.getScriptPath('suggest_subagent.sh', this.config.externalScriptsDir); const contextArg = context ? context.replace(/[\\"]/g, '\\$&') : ''; const result = await (0, secureExecution_1.secureExec)({ command: 'bash', args: contextArg ? ['-c', `"${scriptPath}" "${target}" "${agentType}" "${contextArg}"`] : ['-c', `"${scriptPath}" "${target}" "${agentType}"`], timeout: 10000 }); if (!result.success) { if (result.stderr.includes('not found') || result.stderr.includes('No such file')) { return this.sendSubagentSuggestionManually(target, agentType, context); } throw new Error(`Script execution failed: ${result.stderr}`); } return true; } catch (error) { throw new Error(`Failed to suggest subagent: ${error.message}`); } } async sendSubagentSuggestionManually(target, agentType, context) { let message = ''; switch (agentType) { case 'pm': message = `šŸš€ SUBAGENT SUGGESTION: Consider deploying subagents for parallel execution: • For implementation: Use Task tool with subagent_type='developer' • For testing: Use Task tool with subagent_type='qa-expert' • For research: Use Task tool with subagent_type='research-analyst' Remember: Effective delegation multiplies your impact!`; break; case 'developer': case 'engineer': message = `⚔ PERFORMANCE TIP: Accelerate your work with specialized subagents: • For debugging: Use Task tool with subagent_type='debugger' • For testing: Use Task tool with subagent_type='test-automator' • For review: Use Task tool with subagent_type='code-reviewer' While the subagent investigates, you can continue with other tasks!`; break; default: message = `šŸ’” EFFICIENCY BOOST: Consider using subagents for parallel task execution. Available specialists: debugger, test-automator, performance-engineer, code-reviewer, research-analyst. Deploy with: Task tool with prompt='[your task]' and subagent_type='[agent-type]'`; } if (context) { message = message.replace(':', ` (${context}):`); } return this.sendClaudeMessage(target, message); } async scheduleCheckIn(minutes, note, targetWindow) { try { if (!Number.isInteger(minutes) || minutes < 1 || minutes > 1440) { throw new Error('Minutes must be integer between 1 and 1440 (24 hours)'); } if (!note || typeof note !== 'string') { throw new Error('Invalid note parameter'); } if (note.length > 1000) { throw new Error('Note too long (max 1000 characters)'); } if (targetWindow && (typeof targetWindow !== 'string' || targetWindow.length === 0)) { throw new Error('Invalid targetWindow parameter'); } const scriptPath = await this.pathResolver.getScriptPath('schedule_with_note.sh', this.config.externalScriptsDir); const sanitizedNote = note.replace(/[\\"]/g, '\\$&'); const args = targetWindow ? ['-c', `"${scriptPath}" "${minutes}" "${sanitizedNote}" "${targetWindow}"`] : ['-c', `"${scriptPath}" "${minutes}" "${sanitizedNote}"`]; const result = await (0, secureExecution_1.secureExec)({ command: 'bash', args, timeout: 15000 }); if (!result.success) { throw new Error(`Script execution failed: ${result.stderr}`); } return true; } catch (error) { throw new Error(`Failed to schedule check-in: ${error.message}`); } } async createSession(sessionName, projectPath, windows) { try { if (!sessionName || typeof sessionName !== 'string') { throw new Error('Invalid session name'); } if (!/^[a-zA-Z0-9_-]+$/.test(sessionName)) { throw new Error('Session name can only contain letters, numbers, underscores, and hyphens'); } if (projectPath && typeof projectPath !== 'string') { throw new Error('Invalid project path'); } if (windows && !Array.isArray(windows)) { throw new Error('Windows must be an array'); } const createArgs = projectPath ? ['new-session', '-d', '-s', sessionName, '-c', projectPath] : ['new-session', '-d', '-s', sessionName]; const createResult = await (0, secureExecution_1.secureTmux)('new-session', createArgs.slice(1)); if (!createResult.success) { throw new Error(`Failed to create session: ${createResult.stderr}`); } if (windows && windows.length > 0) { if (windows[0]) { const renameResult = await (0, secureExecution_1.secureTmux)('rename-window', ['-t', `${sessionName}:0`, windows[0]]); if (!renameResult.success) { throw new Error(`Failed to rename first window: ${renameResult.stderr}`); } } for (let i = 1; i < windows.length; i++) { if (typeof windows[i] !== 'string' || windows[i].length === 0) { throw new Error(`Invalid window name at index ${i}`); } const windowArgs = projectPath ? ['new-window', '-t', sessionName, '-n', windows[i], '-c', projectPath] : ['new-window', '-t', sessionName, '-n', windows[i]]; const windowResult = await (0, secureExecution_1.secureTmux)('new-window', windowArgs.slice(1)); if (!windowResult.success) { throw new Error(`Failed to create window ${windows[i]}: ${windowResult.stderr}`); } } } return true; } catch (error) { throw new Error(`Failed to create session: ${error.message}`); } } async killSession(sessionName) { try { if (!sessionName || typeof sessionName !== 'string') { throw new Error('Invalid session name'); } if (!/^[a-zA-Z0-9_-]+$/.test(sessionName)) { throw new Error('Session name can only contain letters, numbers, underscores, and hyphens'); } const result = await (0, secureExecution_1.secureTmux)('kill-session', ['-t', sessionName]); if (!result.success) { throw new Error(`Failed to kill session: ${result.stderr}`); } return true; } catch (error) { throw new Error(`Failed to kill session: ${error.message}`); } } async renameWindow(target, newName) { try { if (!target || typeof target !== 'string') { throw new Error('Invalid target parameter'); } if (!newName || typeof newName !== 'string') { throw new Error('Invalid new name parameter'); } if (newName.length > 100) { throw new Error('Window name too long (max 100 characters)'); } const result = await (0, secureExecution_1.secureTmux)('rename-window', ['-t', target, newName]); if (!result.success) { throw new Error(`Failed to rename window: ${result.stderr}`); } return true; } catch (error) { throw new Error(`Failed to rename window: ${error.message}`); } } async registerQAEngineer(qaEngineerID, publicKeyHex) { try { return await cryptoQA_1.CryptographicQASystem.registerQAEngineer(qaEngineerID, publicKeyHex); } catch (error) { throw new Error(`Failed to register QA Engineer: ${error.message}`); } } async generateQAKeyPair() { try { const keyPair = await cryptoQA_1.CryptographicQASystem.generateQAKeyPair(); const qaEngineerID = `qa_${Date.now()}_${(0, crypto_1.randomBytes)(8).toString('hex')}`; await this.registerQAEngineer(qaEngineerID, keyPair.publicKeyHex); const privateKeyHex = await keyPair.privateKey.useAsync(async (keyData) => { return Buffer.from(keyData).toString('hex'); }); const publicKeyHex = keyPair.publicKeyHex; return { privateKey: privateKeyHex, publicKey: publicKeyHex, qaEngineerID }; } catch (error) { throw new Error(`Failed to generate QA key pair: ${error.message}`); } } async checkQAApproval(projectName, commitHash) { try { if (!projectName || typeof projectName !== 'string') { throw new Error('Invalid project name'); } if (!commitHash || !/^[a-fA-F0-9]{40}$/.test(commitHash)) { throw new Error('Invalid commit hash'); } const approvalKey = `${projectName}:${commitHash}`; const approval = this.approvalCache.get(approvalKey); if (approval) { const verification = await cryptoQA_1.CryptographicQASystem.verifyQAApproval(approval); if (verification.valid) { return { approved: true, details: approval.data, auditHash: cryptoQA_1.CryptographicQASystem.generateAuditHash(approval) }; } else { this.approvalCache.delete(approvalKey); } } const block = this.blockCache.get(approvalKey); if (block) { return { approved: false, details: block }; } return { approved: false, details: { reason: 'No QA approval or block found' } }; } catch (error) { throw new Error(`Failed to check QA approval: ${error.message}`); } } async requestQAValidation(projectName, testTypes, context) { try { const sessions = await this.getTmuxSessions(); const session = sessions.find(s => s.name === projectName); if (!session) { throw new Error(`Project session ${projectName} not found`); } const qaWindow = session.windows.find(w => w.windowName.toLowerCase().includes('qa') || w.windowIndex === 1); if (!qaWindow) { throw new Error('QA Engineer window not found'); } const validationRequest = `šŸ” QA VALIDATION REQUEST Project: ${projectName} Test Types: ${testTypes.join(', ')} ${context ? `Context: ${context}` : ''} Please execute comprehensive testing and provide results: 1. Run automated test suites for: ${testTypes.join(', ')} 2. Perform manual validation as needed 3. Check code quality and security 4. Provide approve/reject decision with detailed feedback Use "Approve for Commit" or "Block Commit" operations based on results.`; await this.sendClaudeMessage(`${projectName}:${qaWindow.windowIndex}`, validationRequest); return true; } catch (error) { throw new Error(`Failed to request QA validation: ${error.message}`); } } async createQAApproval(projectName, commitHash, commitMessage, qaEngineerID, privateKey, testResults, correlationId) { const eventCorrelationId = correlationId || qaEvents_1.qaEventBus.generateCorrelationId(); try { if (!projectName || typeof projectName !== 'string') { throw new Error('Invalid project name'); } if (!commitHash || !/^[a-fA-F0-9]{40}$/.test(commitHash)) { throw new Error('Invalid commit hash'); } if (!commitMessage || typeof commitMessage !== 'string') { throw new Error('Invalid commit message'); } if (!qaEngineerID || typeof qaEngineerID !== 'string') { throw new Error('Invalid QA Engineer ID'); } if (!privateKey || !/^[a-fA-F0-9]{64}$/.test(privateKey)) { throw new Error('Invalid private key format'); } const approvalData = { projectName, commitHash, commitMessage, testResults, qaEngineerID, approvalTimestamp: Date.now(), expirationTimestamp: Date.now() + 30 * 60 * 1000, approvalNonce: '' }; const { SecureKeyManager } = await Promise.resolve().then(() => __importStar(require('./cryptoQA'))); const secureKey = await SecureKeyManager.createSecureKey(privateKey); try { const approval = await cryptoQA_1.CryptographicQASystem.createQAApproval(approvalData, secureKey); const approvalKey = `${projectName}:${commitHash}`; this.approvalCache.set(approvalKey, approval); this.blockCache.delete(approvalKey); await qaEvents_1.qaEventBus.publishEvent({ type: qaEvents_1.QAEventType.APPROVAL_GRANTED, timestamp: Date.now(), projectName, commitHash, qaEngineerID, correlationId: eventCorrelationId, payload: { approval, auditHash: cryptoQA_1.CryptographicQASystem.generateAuditHash(approval), testResults } }); return approval; } finally { secureKey.destroy(); } } catch (error) { await qaEvents_1.qaEventBus.publishEvent({ type: qaEvents_1.QAEventType.QA_SYSTEM_ERROR, timestamp: Date.now(), projectName, commitHash, qaEngineerID, correlationId: eventCorrelationId, payload: { error: error.message, context: 'createQAApproval' } }); throw new Error(`Failed to create QA approval: ${error.message}`); } } async createQABlock(projectName, commitHash, commitMessage, qaEngineerID, blockReason, testResults, privateKey) { try { if (!projectName || typeof projectName !== 'string') { throw new Error('Invalid project name'); } if (!commitHash || !/^[a-fA-F0-9]{40}$/.test(commitHash)) { throw new Error('Invalid commit hash'); } if (!commitMessage || typeof commitMessage !== 'string') { throw new Error('Invalid commit message'); } if (!qaEngineerID || typeof qaEngineerID !== 'string') { throw new Error('Invalid QA Engineer ID'); } if (!blockReason || typeof blockReason !== 'string') { throw new Error('Invalid block reason'); } if (!privateKey || !/^[a-fA-F0-9]{64}$/.test(privateKey)) { throw new Error('Invalid private key format'); } const { SecureKeyManager } = await Promise.resolve().then(() => __importStar(require('./cryptoQA'))); const secureKey = await SecureKeyManager.createSecureKey(privateKey); try { const block = await cryptoQA_1.CryptographicQASystem.createQABlock(projectName, commitHash, commitMessage, qaEngineerID, blockReason, testResults, secureKey); const approvalKey = `${projectName}:${commitHash}`; this.blockCache.set(approvalKey, block); this.approvalCache.delete(approvalKey); return block; } finally { secureKey.destroy(); } } catch (error) { throw new Error(`Failed to create QA block: ${error.message}`); } } async getQAStatus(projectName, commitHash) { try { if (!projectName || typeof projectName !== 'string') { throw new Error('Invalid project name'); } if (!commitHash || !/^[a-fA-F0-9]{40}$/.test(commitHash)) { throw new Error('Invalid commit hash'); } const approvalKey = `${projectName}:${commitHash}`; const approval = this.approvalCache.get(approvalKey); if (approval) { const verification = await cryptoQA_1.CryptographicQASystem.verifyQAApproval(approval); if (verification.valid) { return { status: 'approved', details: approval.data, auditHash: cryptoQA_1.CryptographicQASystem.generateAuditHash(approval) }; } else { this.approvalCache.delete(approvalKey); return { status: 'expired', details: { message: `QA approval invalid: ${verification.reason}`, requiresNewValidation: true } }; } } const block = this.blockCache.get(approvalKey); if (block) { return { status: 'blocked', details: block.blockData }; } return { status: 'pending', details: { message: 'No QA validation performed yet', projectName, commitHash } }; } catch (error) { throw new Error(`Failed to get QA status: ${error.message}`); } } async notifyQAStatusChange(projectName, status, feedback) { try { const sessions = await this.getTmuxSessions(); const session = sessions.find(s => s.name === projectName); if (!session) { return false; } const statusMessage = status === 'approved' ? `āœ… QA APPROVAL: Git commits are now enabled.\n${feedback ? `QA Feedback: ${feedback}` : 'All quality checks passed.'}` : `āŒ QA REJECTION: Git commits are blocked.\n${feedback ? `QA Feedback: ${feedback}` : 'Quality issues found - address and retest.'}`; for (const window of session.windows) { if (window.windowIndex !== 1) { await this.sendClaudeMessage(`${projectName}:${window.windowIndex}`, statusMessage); } } return true; } catch (error) { throw new Error(`Failed to notify QA status change: ${error.message}`); } } async executeQATests(projectName, testTypes, projectPath, commitHash, qaEngineerID) { const correlationId = qaEvents_1.qaEventBus.generateCorrelationId(); try { if (!projectName || typeof projectName !== 'string') { throw new Error('Invalid project name'); } if (!Array.isArray(testTypes) || testTypes.length === 0) { throw new Error('Invalid test types'); } const startTime = Date.now(); await qaEvents_1.qaEventBus.publishEvent({ type: qaEvents_1.QAEventType.TEST_STARTED, timestamp: startTime, projectName, commitHash: commitHash || 'unknown', qaEngineerID, correlationId, payload: { testTypes, projectPath, triggeredBy: qaEngineerID || 'system' } }); const results = { testTypes, executed: [], failed: [], timestamp: new Date().toISOString(), }; const testResults = { unit: false, integration: false, security: false, performance: false, coverage: 0, passedTests: 0, totalTests: testTypes.length, criticalIssues: [] }; for (const testType of testTypes) { try { let testPassed = false; switch (testType) { case 'unit': testPassed = await this.runUnitTests(projectPath); testResults.unit = testPassed; results.executed.push({ type: 'unit', status: testPassed ? 'passed' : 'failed' }); break; case 'integration': testPassed = await this.runIntegrationTests(projectPath); testResults.integration = testPassed; results.executed.push({ type: 'integration', status: testPassed ? 'passed' : 'failed' }); break; case 'security': testPassed = await this.runSecurityScan(projectPath); testResults.security = testPassed; results.executed.push({ type: 'security', status: testPassed ? 'passed' : 'failed' }); break; case 'performance': testPassed = await this.runPerformanceTests(projectPath); testResults.performance = testPassed; results.executed.push({ type: 'performance', status: testPassed ? 'passed' : 'failed' }); break; case 'coverage': testPassed = await this.checkCodeCoverage(projectPath); testResults.coverage = testPassed ? 85 : 60;