@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
JavaScript
"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;