UNPKG

aiwg

Version:

Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.

1,329 lines (1,153 loc) 53.9 kB
/** * Orchestrator for External Ralph Loop * * Main loop logic that coordinates session launching, output analysis, * and state management for the external Ralph supervisor. * * Enhanced with comprehensive state capture for long-running sessions (6-8 hours): * - Pre/post session snapshots (git, .aiwg, file hashes) * - Periodic checkpoints during sessions * - Two-phase state assessment (orient + prompt generation) * - Session transcript and stream-json capture * * Epic #26 Integration: * - PID Control Layer: Dynamic parameter adjustment * - Claude Intelligence Layer: Strategic planning and validation * - Memory Layer: Cross-loop learning and knowledge persistence * - Overseer Layer: Health monitoring and intervention * * @implements @.aiwg/requirements/design-ralph-external.md */ import { writeFileSync, mkdirSync } from 'fs'; import { join, dirname } from 'path'; import { StateManager } from './state-manager.mjs'; import { SessionLauncher } from './session-launcher.mjs'; import { OutputAnalyzer } from './output-analyzer.mjs'; import { PromptGenerator } from './prompt-generator.mjs'; import { SnapshotManager } from './snapshot-manager.mjs'; import { CheckpointManager } from './checkpoint-manager.mjs'; import { StateAssessor } from './state-assessor.mjs'; // Research-backed modules (REF-015 Self-Refine, REF-021 Reflexion) import { BestOutputTracker } from './best-output-tracker.mjs'; import { MemoryManager } from './memory-manager.mjs'; import { EarlyStopping } from './early-stopping.mjs'; import { IterationAnalytics } from './iteration-analytics.mjs'; import { CrossTaskLearner } from './cross-task-learner.mjs'; // Multi-loop coordination (REF-086, REF-088) import { ExternalMultiLoopStateManager } from './external-multi-loop-state-manager.mjs'; // Process reliability (Phase 4) import { ProcessMonitor } from './process-monitor.mjs'; import { RecoveryEngine } from './recovery-engine.mjs'; // ========== EPIC #26 COMPONENTS ========== // PID Control Layer (#23) import { PIDController } from './pid-controller.mjs'; import { GainScheduler } from './gain-scheduler.mjs'; import { MetricsCollector } from './metrics-collector.mjs'; import { ControlAlarms } from './control-alarms.mjs'; // Claude Intelligence Layer (#22) import { ClaudePromptGenerator } from './lib/claude-prompt-generator.mjs'; import { ValidationAgent } from './lib/validation-agent.mjs'; import { StrategyPlanner } from './lib/strategy-planner.mjs'; // Memory Layer (#24) import { SemanticMemory } from './lib/semantic-memory.mjs'; import { MemoryPromotion } from './lib/memory-promotion.mjs'; import { LearningExtractor } from './lib/learning-extractor.mjs'; import { MemoryRetrieval } from './lib/memory-retrieval.mjs'; // Overseer Layer (#25) import { Overseer } from './lib/overseer.mjs'; import { BehaviorDetector } from './lib/behavior-detector.mjs'; import { InterventionSystem } from './lib/intervention-system.mjs'; import { EscalationHandler } from './lib/escalation-handler.mjs'; // Multi-Provider Support import { createProvider, ensureProvidersRegistered } from './lib/provider-adapter.mjs'; /** * @typedef {Object} OrchestratorConfig * @property {string} objective - Task objective * @property {string} completionCriteria - Completion criteria * @property {number} [maxIterations=5] - Maximum external iterations * @property {string} [model='opus'] - Claude model * @property {number} [budgetPerIteration=2.0] - Budget per iteration USD * @property {number} [timeoutMinutes=60] - Timeout per iteration * @property {Object} [mcpConfig] - MCP server configuration * @property {string} [workingDir] - Working directory * @property {Object} [giteaIntegration] - Gitea issue tracking * @property {boolean} [verbose=false] - Enable verbose Claude output * @property {number} [checkpointIntervalMinutes=30] - Checkpoint interval * @property {boolean} [enableCheckpoints=true] - Enable periodic checkpoints * @property {boolean} [enableSnapshots=true] - Enable pre/post session snapshots * @property {boolean} [useClaudeAssessment=false] - Use Claude for state assessment * @property {string[]} [keyFiles=[]] - Key files to track hashes * @property {number} [memory=3] - Memory capacity Ω for MemoryManager (REF-021) * @property {boolean} [crossTask=true] - Enable cross-task learning * @property {boolean} [enableAnalytics=true] - Enable iteration analytics * @property {boolean} [enableBestOutput=true] - Enable best output tracking (REF-015) * @property {boolean} [enableEarlyStopping=true] - Enable early stopping on high confidence * @property {boolean} [force=false] - Force creation despite concurrent loop limit * @property {boolean} [enablePIDControl=true] - Enable PID control system * @property {boolean} [enableOverseer=true] - Enable overseer monitoring * @property {boolean} [enableSemanticMemory=true] - Enable semantic memory * @property {string} [provider='claude'] - CLI provider (claude, codex) */ /** * @typedef {Object} OrchestratorResult * @property {boolean} success - Whether loop completed successfully * @property {string} reason - Reason for completion/failure * @property {number} iterations - Number of iterations executed * @property {string} loopId - Loop identifier */ export class Orchestrator { /** * @param {string} projectRoot - Project root directory */ constructor(projectRoot) { this.projectRoot = projectRoot; this.stateManager = new StateManager(projectRoot); this.sessionLauncher = new SessionLauncher(); this.outputAnalyzer = new OutputAnalyzer(); this.promptGenerator = new PromptGenerator(); this.snapshotManager = new SnapshotManager({ projectRoot }); this.checkpointManager = null; // Created per-session with config this.stateAssessor = new StateAssessor({ projectRoot }); this.aborted = false; this.currentPreSnapshot = null; // Research-backed modules (initialized per-loop in execute()) this.bestOutputTracker = null; this.memoryManager = null; this.earlyStopping = null; this.iterationAnalytics = null; this.crossTaskLearner = null; this.crossTaskLearnings = null; // Multi-loop coordination (initialized lazily) this.multiLoopManager = null; this.registeredLoopId = null; // Process reliability (Phase 4) this.processMonitor = null; this.recoveryEngine = new RecoveryEngine(projectRoot); // ========== EPIC #26 COMPONENTS ========== // PID Control Layer this.pidController = null; this.gainScheduler = null; this.metricsCollector = null; this.controlAlarms = null; // Claude Intelligence Layer this.claudePromptGenerator = null; this.validationAgent = null; this.strategyPlanner = null; // Memory Layer this.semanticMemory = null; this.memoryPromotion = null; this.learningExtractor = null; this.memoryRetrieval = null; // Overseer Layer this.overseer = null; this.behaviorDetector = null; this.interventionSystem = null; this.escalationHandler = null; } /** * Execute the external Ralph loop * @param {OrchestratorConfig} config * @returns {Promise<OrchestratorResult>} */ async execute(config) { // Initialize state with enhanced capture options const state = this.stateManager.initialize({ objective: config.objective, completionCriteria: config.completionCriteria, maxIterations: config.maxIterations || 5, model: config.model || 'opus', budgetPerIteration: config.budgetPerIteration || 2.0, timeoutMinutes: config.timeoutMinutes || 60, mcpConfig: config.mcpConfig, workingDir: config.workingDir || this.projectRoot, giteaIntegration: config.giteaIntegration, // Enhanced capture options verbose: config.verbose || false, checkpointIntervalMinutes: config.checkpointIntervalMinutes || 30, enableCheckpoints: config.enableCheckpoints !== false, enableSnapshots: config.enableSnapshots !== false, useClaudeAssessment: config.useClaudeAssessment || false, keyFiles: config.keyFiles || [], // Research-backed options (REF-015, REF-021) memory: config.memory || 3, crossTask: config.crossTask !== false, enableAnalytics: config.enableAnalytics !== false, enableBestOutput: config.enableBestOutput !== false, enableEarlyStopping: config.enableEarlyStopping !== false, // Epic #26 options enablePIDControl: config.enablePIDControl !== false, enableOverseer: config.enableOverseer !== false, enableSemanticMemory: config.enableSemanticMemory !== false, enableClaudeIntelligence: config.enableClaudeIntelligence !== false, }); console.log(`[External Ralph] Starting loop ${state.loopId}`); console.log(`[External Ralph] Objective: ${config.objective}`); console.log(`[External Ralph] Max iterations: ${state.maxIterations}`); if (state.config.enableSnapshots) { console.log('[External Ralph] Pre/post session snapshots: ENABLED'); } if (state.config.enableCheckpoints) { console.log(`[External Ralph] Periodic checkpoints: ENABLED (${state.config.checkpointIntervalMinutes} min)`); } // ========== MULTI-PROVIDER SUPPORT ========== await ensureProvidersRegistered(); const providerName = config.provider || 'claude'; this.providerAdapter = createProvider(providerName); this.sessionLauncher.setProviderAdapter(this.providerAdapter); this.outputAnalyzer.setProviderAdapter(this.providerAdapter); this.stateAssessor.setProviderAdapter(this.providerAdapter); console.log(`[External Ralph] Provider: ${providerName}`); // ========== MULTI-LOOP COORDINATION (REF-086, REF-088) ========== try { this.multiLoopManager = new ExternalMultiLoopStateManager(this.projectRoot); this.multiLoopManager.ensureBaseDir(); // Check concurrent loop limit const activeLoops = this.multiLoopManager.listActiveLoops(); console.log(`[External Ralph] Active loops: ${activeLoops.length}`); // Register this loop (will enforce limit unless --force was used) const registration = await this.multiLoopManager.createLoop( { objective: config.objective, completionCriteria: config.completionCriteria, maxIterations: config.maxIterations || 5, model: config.model || 'opus', budgetPerIteration: config.budgetPerIteration || 2.0, timeoutMinutes: config.timeoutMinutes || 60, mcpConfig: config.mcpConfig, workingDir: config.workingDir || this.projectRoot, giteaIntegration: config.giteaIntegration, }, { force: config.force || false } ); this.registeredLoopId = registration.loopId; console.log(`[External Ralph] Registered in multi-loop registry: ${registration.loopId}`); } catch (error) { // If multi-loop manager fails, continue with single-loop mode console.warn(`[External Ralph] Multi-loop coordination unavailable: ${error.message}`); console.log('[External Ralph] Continuing in single-loop mode'); } // ========== INITIALIZE RESEARCH MODULES ========== const stateDir = this.stateManager.getStateDir(); // Best Output Tracker (REF-015 Self-Refine) if (state.config.enableBestOutput) { this.bestOutputTracker = new BestOutputTracker(stateDir); console.log('[External Ralph] Best output tracking: ENABLED (REF-015)'); } // Memory Manager (REF-021 Reflexion) this.memoryManager = new MemoryManager({ stateDir, capacity: state.config.memory, }); console.log(`[External Ralph] Memory manager: ENABLED (Ω=${state.config.memory})`); // Early Stopping if (state.config.enableEarlyStopping) { this.earlyStopping = new EarlyStopping({ confidenceThreshold: 0.95, minIterations: 2, requireVerification: true, }); console.log('[External Ralph] Early stopping: ENABLED'); } // Iteration Analytics if (state.config.enableAnalytics) { this.iterationAnalytics = new IterationAnalytics( state.loopId, config.objective, { storagePath: join(stateDir, 'analytics') } ); console.log('[External Ralph] Iteration analytics: ENABLED'); } // Cross-Task Learner if (state.config.crossTask) { this.crossTaskLearner = new CrossTaskLearner({ memory_path: join(this.projectRoot, '.aiwg', 'ralph', 'memory'), }); // Get relevant learnings before starting this.crossTaskLearnings = this.crossTaskLearner.getRelevantLearnings(config.objective); if (this.crossTaskLearnings.similar_tasks.length > 0) { console.log(`[External Ralph] Cross-task learning: Found ${this.crossTaskLearnings.similar_tasks.length} similar tasks`); } else { console.log('[External Ralph] Cross-task learning: ENABLED (no similar tasks found)'); } } // ========== INITIALIZE EPIC #26 COMPONENTS ========== // PID Control Layer if (state.config.enablePIDControl) { this.metricsCollector = new MetricsCollector({ windowSize: 5, integralDecay: 0.9, noiseThreshold: 0.05, }); this.gainScheduler = new GainScheduler(); this.pidController = new PIDController({ kp: 0.5, // Initial proportional gain ki: 0.2, // Initial integral gain kd: 0.3, // Initial derivative gain outputMin: 0, outputMax: 1, }); this.controlAlarms = new ControlAlarms({ projectRoot: this.projectRoot, loopId: state.loopId, }); console.log('[External Ralph] PID control system: ENABLED (#23)'); } // Claude Intelligence Layer if (state.config.enableClaudeIntelligence !== false) { this.claudePromptGenerator = new ClaudePromptGenerator({ projectRoot: this.projectRoot, }); if (this.providerAdapter) { this.claudePromptGenerator.setProviderAdapter(this.providerAdapter); } this.validationAgent = new ValidationAgent({ projectRoot: this.projectRoot, }); this.strategyPlanner = new StrategyPlanner({ projectRoot: this.projectRoot, }); console.log('[External Ralph] Claude intelligence layer: ENABLED (#22)'); } // Memory Layer if (state.config.enableSemanticMemory) { const memoryPath = join(this.projectRoot, '.aiwg', 'ralph', 'semantic-memory'); this.semanticMemory = new SemanticMemory({ storagePath: memoryPath, }); this.memoryPromotion = new MemoryPromotion({ storagePath: memoryPath, }); this.learningExtractor = new LearningExtractor({ projectRoot: this.projectRoot, }); this.memoryRetrieval = new MemoryRetrieval({ storagePath: memoryPath, }); console.log('[External Ralph] Semantic memory layer: ENABLED (#24)'); } // Overseer Layer if (state.config.enableOverseer) { this.behaviorDetector = new BehaviorDetector({ projectRoot: this.projectRoot, }); this.interventionSystem = new InterventionSystem({ projectRoot: this.projectRoot, }); this.escalationHandler = new EscalationHandler({ projectRoot: this.projectRoot, loopId: state.loopId, }); this.overseer = new Overseer({ projectRoot: this.projectRoot, loopId: state.loopId, behaviorDetector: this.behaviorDetector, interventionSystem: this.interventionSystem, escalationHandler: this.escalationHandler, }); console.log('[External Ralph] Overseer monitoring: ENABLED (#25)'); } // ========== PROCESS MONITOR (Phase 4) ========== this.processMonitor = new ProcessMonitor({ projectRoot: this.projectRoot, heartbeatIntervalMs: 30000, // 30 seconds staleThresholdMs: 120000, // 2 minutes }); // Record initial heartbeat this.processMonitor.recordHeartbeat(state.loopId, { iteration: 0, status: 'starting', }); console.log('[External Ralph] Process monitoring: ENABLED'); return this.runLoop(state); } /** * Resume an interrupted loop * @param {Object} [overrides] - Configuration overrides * @returns {Promise<OrchestratorResult>} */ async resume(overrides = {}) { const state = this.stateManager.load(); if (!state) { throw new Error('No external Ralph loop to resume'); } console.log(`[External Ralph] Resuming loop ${state.loopId}`); console.log(`[External Ralph] Current iteration: ${state.currentIteration}`); // ========== CRASH RECOVERY (Phase 4) ========== const crashState = this.recoveryEngine.detectCrash(); if (crashState.crashed) { console.log(`[External Ralph] Crash detected at iteration ${crashState.iteration}`); console.log(`[External Ralph] Recovery strategy: ${crashState.recoveryStrategy?.type || 'continue'}`); // Log recovery attempt this.recoveryEngine.notifyCrash(state.loopId, new Error('Process crash detected during resume')); // Check for checkpoint recovery const latestCheckpoint = this.recoveryEngine.getLatestCheckpoint(state.loopId); if (latestCheckpoint) { console.log(`[External Ralph] Latest checkpoint: iteration ${latestCheckpoint.iteration}`); // Could restore from checkpoint here if needed // For now, continue from current state with accumulated learnings } // Mark as recovering (will be marked as running once loop starts) state.status = 'recovering'; state.recoveryAttempts = (state.recoveryAttempts || 0) + 1; state.lastRecoveryAt = new Date().toISOString(); } // Apply overrides if (overrides.maxIterations) { state.maxIterations = overrides.maxIterations; } if (overrides.budgetPerIteration) { state.config.budgetPerIteration = overrides.budgetPerIteration; } state.status = 'running'; this.stateManager.save(state); // Mark recovery complete if we were recovering if (crashState.crashed) { this.recoveryEngine.markRecovered(); console.log('[External Ralph] Recovery marked complete'); } // Initialize process monitor for resumed loop this.processMonitor = new ProcessMonitor({ projectRoot: this.projectRoot, heartbeatIntervalMs: 30000, staleThresholdMs: 120000, }); this.processMonitor.recordHeartbeat(state.loopId, { iteration: state.currentIteration, status: 'resumed', }); return this.runLoop(state); } /** * Main loop execution * @param {Object} state - Loop state * @returns {Promise<OrchestratorResult>} */ async runLoop(state) { while (state.currentIteration < state.maxIterations && !this.aborted) { state.currentIteration++; // Save the updated iteration count immediately this.stateManager.update({ currentIteration: state.currentIteration }); console.log(`\n[External Ralph] === Iteration ${state.currentIteration}/${state.maxIterations} ===`); // Record iteration heartbeat if (this.processMonitor) { this.processMonitor.recordHeartbeat(state.loopId, { iteration: state.currentIteration, status: 'running', }); } // Get output paths and iteration directory const outputPaths = this.stateManager.getOutputPaths(state.currentIteration); const iterationDir = dirname(outputPaths.stdout); mkdirSync(iterationDir, { recursive: true }); try { // ========== PRE-SESSION SNAPSHOT ========== if (state.config.enableSnapshots) { console.log('[External Ralph] Capturing pre-session snapshot...'); this.currentPreSnapshot = await this.snapshotManager.capturePreSnapshot({ sessionId: state.sessionId, iteration: state.currentIteration, objective: state.objective, keyFiles: state.config.keyFiles, }); // Save snapshot to iteration directory writeFileSync( join(iterationDir, 'pre-snapshot.json'), JSON.stringify(this.currentPreSnapshot, null, 2) ); } // ========== PRE-ITERATION: INTELLIGENCE & MEMORY ========== let prompt, systemPrompt; const promptType = state.currentIteration === 1 ? 'initial' : 'continuation'; const lastIteration = state.iterations[state.iterations.length - 1]; // StateAssessor: Assess current situation let assessment = null; if (state.currentIteration > 1) { console.log('[External Ralph] Assessing current state...'); assessment = await this.stateAssessor.assess({ stdoutPath: lastIteration?.stdoutFile, stderrPath: lastIteration?.stderrFile, exitCode: lastIteration?.exitCode || 0, timedOut: false, preSnapshot: this.currentPreSnapshot, postSnapshot: lastIteration?.postSnapshot, objective: state.objective, completionCriteria: state.completionCriteria, iteration: state.currentIteration, maxIterations: state.maxIterations, accumulatedLearnings: state.accumulatedLearnings, outputDir: iterationDir, }); // Save assessment writeFileSync( join(iterationDir, 'state-assessment.json'), JSON.stringify(assessment, null, 2) ); } // MemoryRetrieval: Get relevant cross-loop knowledge let relevantKnowledge = null; if (this.memoryRetrieval && state.currentIteration > 1) { console.log('[External Ralph] Retrieving relevant knowledge...'); relevantKnowledge = await this.memoryRetrieval.retrieve({ query: state.objective, context: assessment?.summary || '', maxResults: 5, }); } // StrategyPlanner: Plan strategy for this iteration let strategy = null; if (this.strategyPlanner) { console.log('[External Ralph] Planning iteration strategy...'); strategy = await this.strategyPlanner.plan({ objective: state.objective, completionCriteria: state.completionCriteria, iteration: state.currentIteration, maxIterations: state.maxIterations, assessment, relevantKnowledge, previousIterations: state.iterations, }); } // PID Controller: Compute control signals let controlSignals = null; if (this.pidController && this.metricsCollector && state.currentIteration > 1) { // Extract metrics from last iteration const metrics = this.metricsCollector.extractIterationMetrics(lastIteration); const pidMetrics = this.metricsCollector.computePIDMetrics(metrics); // Adjust gains based on situation if (this.gainScheduler && assessment) { const adjustedGains = this.gainScheduler.adjustGains({ phase: assessment.phase || 'normal', volatility: pidMetrics.derivative, errorMagnitude: pidMetrics.proportional, }); this.pidController.updateGains(adjustedGains); } // Compute control output controlSignals = this.pidController.compute(pidMetrics.proportional); console.log(`[External Ralph] PID control: P=${pidMetrics.proportional.toFixed(3)}, output=${controlSignals.output.toFixed(3)}`); } // ValidationAgent: Pre-iteration validation let preValidation = null; if (this.validationAgent) { console.log('[External Ralph] Running pre-iteration validation...'); preValidation = await this.validationAgent.validatePre({ objective: state.objective, completionCriteria: state.completionCriteria, iteration: state.currentIteration, strategy, assessment, }); if (!preValidation.valid) { console.warn(`[External Ralph] Pre-validation warnings: ${preValidation.warnings.length}`); } } // ClaudePromptGenerator: Generate optimized prompt if (this.claudePromptGenerator) { console.log('[External Ralph] Generating Claude-optimized prompt...'); const promptResult = await this.claudePromptGenerator.generate({ objective: state.objective, completionCriteria: state.completionCriteria, iteration: state.currentIteration, maxIterations: state.maxIterations, assessment, strategy, relevantKnowledge, controlSignals, previousIterations: state.iterations, }); prompt = promptResult.prompt; systemPrompt = promptResult.systemPrompt; } else { // Fallback to standard prompt generator // Get reflection context from MemoryManager (REF-021) const reflectionContext = this.memoryManager ? this.memoryManager.getContextForPrompt() : ''; // Get cross-task learnings context const crossTaskContext = this.crossTaskLearnings?.context_summary || ''; if (promptType === 'continuation' && state.config.useClaudeAssessment) { // Use two-phase state assessment for continuation prompts console.log('[External Ralph] Performing two-phase state assessment...'); const fallbackAssessment = await this.stateAssessor.assess({ stdoutPath: lastIteration?.stdoutFile, stderrPath: lastIteration?.stderrFile, exitCode: lastIteration?.exitCode || 0, timedOut: false, preSnapshot: this.currentPreSnapshot, postSnapshot: lastIteration?.postSnapshot, objective: state.objective, completionCriteria: state.completionCriteria, iteration: state.currentIteration, maxIterations: state.maxIterations, accumulatedLearnings: state.accumulatedLearnings, outputDir: iterationDir, }); // Save assessment to iteration directory writeFileSync( join(iterationDir, 'state-assessment.json'), JSON.stringify(fallbackAssessment, null, 2) ); prompt = fallbackAssessment.prompt; systemPrompt = this.promptGenerator.buildSystemPrompt({ objective: state.objective, completionCriteria: state.completionCriteria, iteration: state.currentIteration, maxIterations: state.maxIterations, loopId: state.loopId, }); } else { // Use standard prompt generator const generated = this.promptGenerator.build({ type: promptType, objective: state.objective, completionCriteria: state.completionCriteria, iteration: state.currentIteration, maxIterations: state.maxIterations, loopId: state.loopId, sessionId: state.sessionId, learnings: state.accumulatedLearnings, filesModified: state.filesModified, previousStatus: lastIteration?.analysis?.success ? 'partial' : 'incomplete', previousOutput: lastIteration?.analysis?.learnings, lastAnalysis: lastIteration?.analysis?.nextApproach, // Research-backed context injection (REF-015, REF-021) reflectionContext: reflectionContext, crossTaskContext: crossTaskContext, }); prompt = generated.prompt; systemPrompt = generated.systemPrompt; } } // Save prompt for debugging const promptPath = this.stateManager.getPromptPath(state.currentIteration); mkdirSync(dirname(promptPath), { recursive: true }); writeFileSync(promptPath, prompt); // ========== START CHECKPOINT MANAGER ========== if (state.config.enableCheckpoints) { console.log('[External Ralph] Starting checkpoint manager...'); this.checkpointManager = new CheckpointManager({ stateDir: iterationDir, projectRoot: this.projectRoot, interval: state.config.checkpointIntervalMinutes * 60 * 1000, sessionId: state.sessionId, }); this.checkpointManager.start(); } // ========== LAUNCH SESSION ========== console.log('[External Ralph] Launching Claude session...'); const startTime = Date.now(); this.stateManager.setCurrentPid(null); const sessionResult = await this.sessionLauncher.launch({ prompt, sessionId: state.sessionId, model: state.config.model, budget: state.config.budgetPerIteration, systemPrompt, mcpConfig: state.config.mcpConfig, workingDir: state.config.workingDir, stdoutPath: outputPaths.stdout, stderrPath: outputPaths.stderr, outputDir: iterationDir, timeoutMs: state.config.timeoutMinutes * 60 * 1000, verbose: state.config.verbose, }); const duration = Date.now() - startTime; // ========== STOP CHECKPOINT MANAGER ========== let checkpointSummary = null; if (this.checkpointManager) { console.log('[External Ralph] Stopping checkpoint manager...'); checkpointSummary = this.checkpointManager.stop(); this.checkpointManager = null; } console.log(`[External Ralph] Session completed (${Math.round(duration / 1000)}s, exit: ${sessionResult.exitCode})`); if (sessionResult.toolCallCount) { console.log(`[External Ralph] Tool calls: ${sessionResult.toolCallCount}, errors: ${sessionResult.errorCount || 0}`); } // Record session completion heartbeat if (this.processMonitor) { this.processMonitor.recordHeartbeat(state.loopId, { iteration: state.currentIteration, status: 'analyzing', }); } // ========== POST-SESSION SNAPSHOT ========== let postSnapshot = null; if (state.config.enableSnapshots && this.currentPreSnapshot) { console.log('[External Ralph] Capturing post-session snapshot...'); postSnapshot = await this.snapshotManager.capturePostSnapshot(this.currentPreSnapshot); // Save snapshot and diff writeFileSync( join(iterationDir, 'post-snapshot.json'), JSON.stringify(postSnapshot, null, 2) ); const snapshotDiff = this.snapshotManager.calculateDiff(this.currentPreSnapshot, postSnapshot); writeFileSync( join(iterationDir, 'snapshot-diff.json'), JSON.stringify(snapshotDiff, null, 2) ); if (snapshotDiff.summary.totalChanges > 0) { console.log(`[External Ralph] Changes detected: ${snapshotDiff.summary.totalChanges} (git: ${snapshotDiff.summary.gitChanges}, aiwg: ${snapshotDiff.summary.aiwgChanges})`); } } // ========== ANALYZE OUTPUT ========== console.log('[External Ralph] Analyzing output...'); const analysis = await this.outputAnalyzer.analyze({ stdoutPath: outputPaths.stdout, stderrPath: outputPaths.stderr, exitCode: sessionResult.exitCode, context: { objective: state.objective, criteria: state.completionCriteria, }, }); // Save analysis this.stateManager.saveAnalysis(state.currentIteration, analysis); // ========== POST-ITERATION: VALIDATION & OVERSIGHT ========== // ValidationAgent: Post-iteration validation let postValidation = null; if (this.validationAgent) { console.log('[External Ralph] Running post-iteration validation...'); postValidation = await this.validationAgent.validatePost({ objective: state.objective, completionCriteria: state.completionCriteria, iteration: state.currentIteration, analysis, sessionResult, preValidation, }); if (!postValidation.valid) { console.warn(`[External Ralph] Post-validation issues: ${postValidation.errors.length}`); } } // Overseer: Health check and intervention let healthReport = null; let interventionResult = null; if (this.overseer) { console.log('[External Ralph] Running overseer health check...'); healthReport = await this.overseer.check({ iteration: state.currentIteration, analysis, validation: postValidation, metrics: this.metricsCollector ? this.metricsCollector.extractIterationMetrics({ analysis, ...sessionResult, }) : null, }); // Handle interventions if (healthReport.intervention !== 'NONE') { console.log(`[External Ralph] Intervention triggered: ${healthReport.intervention}`); interventionResult = await this.interventionSystem.intervene(healthReport); // Check if we need to pause/abort if (healthReport.intervention === 'PAUSE' || healthReport.intervention === 'ABORT') { const escalation = await this.escalationHandler.escalate({ level: healthReport.intervention === 'ABORT' ? 'critical' : 'high', reason: healthReport.reason, context: { iteration: state.currentIteration, healthReport, analysis, }, }); if (healthReport.intervention === 'ABORT') { state.status = 'aborted'; state.abortReason = healthReport.reason; this.stateManager.save(state); await this.completeMultiLoop('aborted'); return { success: false, reason: `Aborted by overseer: ${healthReport.reason}`, iterations: state.currentIteration, loopId: state.loopId, }; } } } } // ========== UPDATE STATE ========== state = this.stateManager.addIteration({ number: state.currentIteration, sessionId: state.sessionId, promptFile: promptPath, stdoutFile: outputPaths.stdout, stderrFile: outputPaths.stderr, exitCode: sessionResult.exitCode, duration, status: analysis.completed ? 'completed' : 'incomplete', analysis, learnings: analysis.learnings ? [analysis.learnings] : [], filesModified: analysis.artifactsModified || [], progress: analysis.nextApproach, // Enhanced capture data preSnapshot: this.currentPreSnapshot, postSnapshot, checkpointSummary, transcriptPath: sessionResult.transcriptPath, parsedEventsPath: sessionResult.parsedEventsPath, toolCallCount: sessionResult.toolCallCount, errorCount: sessionResult.errorCount, // Epic #26 data assessment, strategy, controlSignals, preValidation, postValidation, healthReport, interventionResult, }); console.log(`[External Ralph] Analysis: completed=${analysis.completed}, success=${analysis.success}, progress=${analysis.completionPercentage}%`); // ========== RECORD TO RESEARCH MODULES ========== const qualityScore = (analysis.completionPercentage || 0) / 100; const verificationPassed = analysis.completed && analysis.success; // Best Output Tracker (REF-015) if (this.bestOutputTracker) { this.bestOutputTracker.recordIteration({ iteration: state.currentIteration, artifacts: analysis.artifactsModified || [], qualityScore, validationPassed: verificationPassed, }); } // Memory Manager - add reflection (REF-021) if (this.memoryManager && analysis.learnings) { this.memoryManager.addReflection({ iteration: state.currentIteration, content: analysis.learnings, type: analysis.completed ? 'success_pattern' : 'strategy_change', effectiveness: verificationPassed ? 'helpful' : 'neutral', }); } // Early Stopping if (this.earlyStopping) { this.earlyStopping.recordIterationResult({ iteration: state.currentIteration, qualityScore, verificationPassed, }); } // Iteration Analytics if (this.iterationAnalytics) { this.iterationAnalytics.recordIteration({ iteration_number: state.currentIteration, quality_score: qualityScore * 100, tokens_used: sessionResult.toolCallCount || 0, token_cost_usd: 0, // Would need actual cost tracking execution_time_ms: duration, verification_status: verificationPassed ? 'passed' : 'failed', output_snapshot_path: outputPaths.stdout, reflections: analysis.learnings ? [analysis.learnings] : [], }); } // LearningExtractor & MemoryPromotion if (this.learningExtractor && this.memoryPromotion && verificationPassed) { console.log('[External Ralph] Extracting and promoting learnings...'); const learnings = await this.learningExtractor.extract({ iteration: state.currentIteration, analysis, strategy, outcome: 'success', }); if (learnings.length > 0) { await this.memoryPromotion.promote({ learnings, source: `loop-${state.loopId}-iteration-${state.currentIteration}`, }); } } // ========== CHECK EARLY STOPPING ========== if (this.earlyStopping && state.currentIteration >= 2) { const earlyStopResult = this.earlyStopping.shouldStop(); if (earlyStopResult.stop) { console.log(`[External Ralph] Early stopping triggered: ${earlyStopResult.reason}`); // Select best output before completing const selectedIteration = this.selectBestOutput(state); if (selectedIteration !== state.currentIteration) { console.log(`[External Ralph] Selected iteration ${selectedIteration} as best output`); } state.status = 'completed'; this.stateManager.save(state); await this.generateCompletionReport(state, 'early_stop'); await this.recordTaskCompletion(state, 'success'); await this.completeMultiLoop('completed'); return { success: true, reason: `Early stop: ${earlyStopResult.reason}`, iterations: state.currentIteration, loopId: state.loopId, }; } } // ========== CHECK COMPLETION ========== if (analysis.completed && analysis.success) { // Select best output (may not be final iteration per REF-015) const selectedIteration = this.selectBestOutput(state); if (selectedIteration !== state.currentIteration) { console.log(`[External Ralph] Selected iteration ${selectedIteration} as best output (not final)`); } state.status = 'completed'; this.stateManager.save(state); await this.generateCompletionReport(state, 'success'); await this.recordTaskCompletion(state, 'success'); await this.completeMultiLoop('completed'); return { success: true, reason: 'Task completed successfully', iterations: state.currentIteration, loopId: state.loopId, selectedIteration, }; } // Check if we should continue if (!analysis.shouldContinue) { state.status = 'failed'; this.stateManager.save(state); await this.generateCompletionReport(state, 'blocked'); await this.completeMultiLoop('failed'); return { success: false, reason: analysis.failureClass || 'Cannot continue', iterations: state.currentIteration, loopId: state.loopId, }; } console.log(`[External Ralph] Will continue with: ${analysis.nextApproach}`); } catch (error) { console.error(`[External Ralph] Iteration ${state.currentIteration} error:`, error.message); // Stop checkpoint manager if running if (this.checkpointManager) { try { this.checkpointManager.stop(); } catch (e) { // Ignore stop errors } this.checkpointManager = null; } // Save error state this.stateManager.addIteration({ number: state.currentIteration, sessionId: state.sessionId, exitCode: -1, duration: 0, status: 'error', analysis: { error: error.message }, learnings: [`Error: ${error.message}`], filesModified: [], progress: 'Error recovery needed', preSnapshot: this.currentPreSnapshot, }); // Continue to next iteration (crash recovery) console.log('[External Ralph] Will retry in next iteration'); } } // Loop ended without success if (this.aborted) { // Stop checkpoint manager if running if (this.checkpointManager) { try { this.checkpointManager.stop(); } catch (e) { // Ignore stop errors } this.checkpointManager = null; } state.status = 'aborted'; this.stateManager.save(state); await this.completeMultiLoop('aborted'); return { success: false, reason: 'Aborted by user', iterations: state.currentIteration, loopId: state.loopId, }; } // Select best output even on limit reached (REF-015) const selectedIteration = this.selectBestOutput(state); if (selectedIteration !== state.currentIteration) { console.log(`[External Ralph] Selected iteration ${selectedIteration} as best output (limit reached)`); } state.status = 'limit_reached'; this.stateManager.save(state); await this.generateCompletionReport(state, 'limit'); await this.recordTaskCompletion(state, 'partial'); await this.completeMultiLoop('limit_reached'); return { success: false, reason: 'Maximum iterations reached', iterations: state.currentIteration, loopId: state.loopId, selectedIteration, }; } /** * Generate completion report * @param {Object} state - Final state * @param {string} status - Completion status */ async generateCompletionReport(state, status) { const reportPath = join(this.stateManager.getStateDir(), 'completion-report.md'); const iterations = state.iterations.map((iter, idx) => { return `| ${idx + 1} | ${iter.status} | ${iter.duration}ms | ${iter.analysis?.completionPercentage || 0}% |`; }).join('\n'); const report = `# External Ralph Loop Completion Report ## Summary | Property | Value | |----------|-------| | Loop ID | ${state.loopId} | | Status | ${status} | | Iterations | ${state.currentIteration} | | Start Time | ${state.startTime} | | End Time | ${new Date().toISOString()} | ## Objective ${state.objective} ## Completion Criteria ${state.completionCriteria} ## Iterations | # | Status | Duration | Progress | |---|--------|----------|----------| ${iterations} ## Accumulated Learnings ${state.accumulatedLearnings || 'None recorded'} ## Files Modified ${state.filesModified.length > 0 ? state.filesModified.map(f => `- ${f}`).join('\n') : 'None recorded'} ## Final Status **${status.toUpperCase()}** `; writeFileSync(reportPath, report); console.log(`[External Ralph] Report saved to: ${reportPath}`); } /** * Abort the loop */ abort() { this.aborted = true; this.sessionLauncher.kill(); // Stop process monitor if (this.processMonitor) { this.processMonitor.stopAll(); } // Stop checkpoint manager if running if (this.checkpointManager) { try { this.checkpointManager.stop(); } catch { // Ignore stop errors } this.checkpointManager = null; } console.log('[External Ralph] Abort requested'); } /** * Graceful shutdown with state persistence * Called on SIGTERM/SIGINT for clean termination */ async gracefulShutdown() { console.log('[External Ralph] Graceful shutdown initiated...'); // Record final heartbeat const state = this.stateManager.load(); if (state && this.processMonitor) { this.processMonitor.recordHeartbeat(state.loopId, { iteration: state.currentIteration, status: 'shutting_down', }); } // Stop checkpoint manager and save summary let checkpointSummary = null; if (this.checkpointManager) { console.log('[External Ralph] Stopping checkpoint manager...'); try { checkpointSummary = this.checkpointManager.stop(); } catch { // Ignore stop errors } this.checkpointManager = null; } // Save interrupted state with checkpoint summary if (state) { state.status = 'interrupted'; state.interruptedAt = new Date().toISOString(); if (checkpointSummary) { state.lastCheckpointSummary = checkpointSummary; } this.stateManager.save(state); console.log(`[External Ralph] State saved (iteration ${state.currentIteration})`); } // Stop process monitor if (this.processMonitor) { this.processMonitor.stopAll(); } // Complete multi-loop registration await this.completeMultiLoop('interrupted'); // Kill Claude session gracefully this.sessionLauncher.kill(); console.log('[External Ralph] Graceful shutdown complete'); } /** * Get current status * @returns {Object|null} */ getStatus() { return this.stateManager.load(); } /** * Select and apply the best output from iteration history (REF-015 Self-Refine) * @param {Object} state - Current loop state * @returns {Promise<Object>} Selection result with best iteration info */ async selectBestOutput(state) { console.log('[External Ralph] Selecting best output from iteration history...'); let bestIteration = state.currentIteration; let selectionSource = 'final'; // Try BestOutputTracker first (quality dimensions approach) if (this.bestOutputTracker) { try { const bestOutput = this.bestOutputTracker.selectOutput(); if (bestOutput && bestOutput.iteration !== state.currentIteration) { bestIteration = bestOutput.iteration; selectionSource = 'best-output-tracker'; console.log(`[External Ralph] BestOutputTracker selected iteration ${bestIteration} (score: ${bestOutput.quality_score})`); } } catch (error) { console.warn('[External Ralph] BestOutputTracker selection failed:', error.message); } } // Fallback to IterationAnalytics if available if (selectionSource === 'final' && this.iterationAnalytics) { try { const bestFromAnalytics = this.iterationAnalytics.selectBestIteration(); if (bestFromAnalytics && bestFromAnalytics.iteration_number !== state.currentIteration) { bestIteration = bestFromAnalytics.iteration_number; selectionSource = 'iteration-analytics'; console.log(`[External Ralph] IterationAnalytics selected iteration ${bestIteration} (quality: ${bestFromAnalytics.quality_score})`); } } catch (error) { console.warn('[External Ralph] IterationAnalytics selection failed:', error.message); } } // If best iteration differs from final, restore artifacts from that iteration if (bestIteration !== state.currentIteration) { console.log(`[External Ralph] Restoring artifacts from iteration ${bestIteration} (better than final iteration ${state.currentIteration})`); // Note: Actual artifact restoration would require snapshot management // For now, we record the selection decision state.bestIterationSelected = bestIteration; state.selectionSource = selectionSource; } else { console.log('[External Ralph] Final iteration is the best output'); state.bestIterationSelected = state.currentIteration; state.selectionSource = 'final'; } return { bestIteration, selectionSource, currentIteration: state.currentIteration, wasDifferent: bestIteration !== state.currentIteration, }; } /** * Record task completion for cross-task learning (REF-154) * @param {Object} state - Final loop state * @param {string} outcome - 'success' | 'failure' | 'partial' * @returns {Promise<void>} */ async recordTaskCompletion(state, outcome) { console.log(`[External Ralph] Recording task completion (outcome: ${outcome})...`); // Extract final learnings for semantic memory if (this.learningExtractor && this.semanticMemory && state.iterations.length >