@sirmrmarty/n8n-nodes-tmux-orchestrator
Version:
n8n nodes for orchestrating Claude AI agents through tmux sessions
419 lines (411 loc) • 18.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProjectOrchestrator = void 0;
const conditionWaiter_1 = require("./conditionWaiter");
const resourceManager_1 = require("./resourceManager");
const circuitBreaker_1 = require("./circuitBreaker");
const threadSafeState_1 = require("./threadSafeState");
class ProjectOrchestrator {
constructor(bridge, config = {}) {
this.monitoredProjects = threadSafeState_1.stateManager.getMap('monitoredProjects');
this.isMonitoring = false;
this.bridge = bridge;
this.config = {
checkInterval: 60000,
maxRetries: 5,
autoCreatePR: true,
...config,
};
}
async addProject(projectName) {
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`);
if (!this.isMonitoring) {
this.startMonitoring();
}
}
catch (error) {
console.error(`Failed to add project ${projectName} to monitoring:`, error.message);
throw error;
}
}
}
async removeProject(projectName) {
try {
const deleted = await this.monitoredProjects.safeDelete(projectName);
if (deleted) {
console.log(`Removed project ${projectName} from monitoring`);
}
if (this.monitoredProjects.safeSize() === 0) {
await this.stopMonitoring();
}
}
catch (error) {
console.error(`Failed to remove project ${projectName} from monitoring:`, error.message);
throw error;
}
}
startMonitoring() {
if (this.isMonitoring) {
return;
}
console.log('Starting autonomous project monitoring...');
this.isMonitoring = true;
const { id } = resourceManager_1.resourceManager.createInterval(async () => {
await this.checkAllProjects();
}, this.config.checkInterval, 'Project orchestrator monitoring');
this.monitoringResourceId = id;
}
async stopMonitoring() {
if (this.monitoringResourceId) {
await resourceManager_1.resourceManager.cleanup(this.monitoringResourceId);
this.monitoringResourceId = undefined;
}
this.isMonitoring = false;
console.log('Stopped autonomous project monitoring');
}
async checkAllProjects() {
const circuitBreaker = circuitBreaker_1.circuitRegistry.getBreaker('project-monitoring', {
failureThreshold: 5,
recoveryTimeout: 180000,
successThreshold: 3,
monitoringWindow: 600000,
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 () => {
console.warn('[Circuit Breaker] Project monitoring is in open state, skipping monitoring cycle');
});
}
async checkProject(projectName) {
const circuitBreaker = circuitBreaker_1.circuitRegistry.getBreaker(`project-check-${projectName}`, {
failureThreshold: 3,
recoveryTimeout: 120000,
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 () => {
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}`);
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);
}
}
});
}
async performProjectCheck(projectName) {
const status = this.monitoredProjects.safeGet(projectName);
if (!status || status.prCreated) {
return;
}
try {
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;
}
const completionStatus = await this.checkProjectCompletion(projectName);
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;
}
const currentStatus = this.monitoredProjects.safeGet(projectName);
if (!currentStatus) {
return;
}
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}`);
await this.notifyProjectCompletion(projectName, prResult.prUrl);
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);
}
}
}
async checkProjectCompletion(projectName) {
const circuitBreaker = circuitBreaker_1.circuitRegistry.getBreaker(`completion-check-${projectName}`, {
failureThreshold: 3,
recoveryTimeout: 90000,
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 () => {
console.warn(`[Circuit Breaker] Completion check for ${projectName} failed, returning safe status`);
return {
isComplete: false,
qaApproved: false,
readyForPR: false
};
});
}
async performCompletionCheck(projectName) {
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.');
await conditionWaiter_1.ConditionWaiter.waitForPrompt(this.bridge, projectName, 0);
const output = await this.bridge.captureWindowContent(projectName, 0, 20);
let response = '';
if (typeof output === 'string') {
response = output.split('\n').slice(-10).join('\n').toLowerCase();
}
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));
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'));
return {
isComplete,
qaApproved,
readyForPR: isComplete && qaApproved,
};
}
async createAutomaticPR(projectName) {
const circuitBreaker = circuitBreaker_1.circuitRegistry.getBreaker(`pr-creation-${projectName}`, {
failureThreshold: 2,
recoveryTimeout: 300000,
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 () => {
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'
};
});
}
async performPRCreation(projectName) {
try {
await this.bridge.sendCommandToWindow(projectName, 0, 'pwd');
await conditionWaiter_1.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');
}
const branchCmd = `cd ${projectPath} && git branch --show-current`;
await this.bridge.sendCommandToWindow(projectName, 0, branchCmd);
await conditionWaiter_1.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();
}
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_1.ConditionWaiter.waitForGitOperation(this.bridge, projectName, 0, 'push');
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,
};
}
}
generatePRDescription(projectName) {
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');
}
async notifyProjectCompletion(projectName, prUrl) {
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();
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);
}
}
getMonitoringStatus() {
return {
isMonitoring: this.isMonitoring,
projectCount: this.monitoredProjects.safeSize(),
projects: this.monitoredProjects.safeValues(),
};
}
async destroy() {
await this.stopMonitoring();
try {
await this.monitoredProjects.safeClear();
}
catch (error) {
console.error('Failed to clear monitored projects:', error.message);
}
console.log('Project orchestrator destroyed');
}
}
exports.ProjectOrchestrator = ProjectOrchestrator;
//# sourceMappingURL=projectOrchestrator.js.map