UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes CodeSearch (hybrid SQLite + pgvector), mem0/memgraph specialists, and all CFN skills.

1,390 lines (1,190 loc) 44.5 kB
/** * CFN Loop Orchestrator - Complete TypeScript Implementation * Orchestrates the Fail Never (CFN) Loop workflow with test-driven validation * Supports MVP, Standard, and Enterprise execution modes * * Version: 3.0.0 */ import { gateCheck, GateCheckParams } from './helpers/gate-check'; import { collectConsensus, validateConsensus } from './helpers/consensus'; import { spawnLoop3Agents, spawnLoop2Agents, SpawnResult } from './helpers/spawn-agents'; import { TestResult, ExecutionMode } from './types'; import { execSync } from 'child_process'; import * as path from 'path'; import * as fs from 'fs/promises'; /** * Execution phases in the CFN Loop */ export type LoopPhase = 'loop3' | 'loop2' | 'product-owner' | 'complete'; /** * Product owner decision outcomes */ export type ProductOwnerDecision = 'PROCEED' | 'ITERATE' | 'ABORT' | null; /** * Timeout configuration for agent execution */ export interface TimeoutConfig { loop3Agent?: number; loop2Agent?: number; productOwner?: number; } /** * Orchestration configuration */ export interface OrchestrationConfig { taskId: string; mode: ExecutionMode; maxIterations: number; aceReflect?: boolean; loop3Agents?: string[]; loop2Agents?: string[]; productOwner?: string; successCriteriaEnabled?: boolean; timeouts?: TimeoutConfig; workspace?: string; taskDescription?: string; } /** * Agent execution context */ export interface AgentExecutionContext { agentId: string; agentType: string; loopType: 'loop3' | 'loop2'; iteration: number; taskId: string; timestamp: number; } /** * Phase transition tracking */ export interface PhaseTransition { fromPhase: LoopPhase; toPhase: LoopPhase; timestamp: number; iteration: number; } /** * Gate check result */ export interface GateCheckResult { passed: boolean; passRate: number; threshold: number; gap: number; } /** * Consensus validation result */ export interface ConsensusValidationResult { passed: boolean; average: number; threshold: number; gap: number; } /** * Test result aggregation */ export interface AggregatedTestResults { totalPass: number; totalFail: number; totalSkip: number; passRate: number; agentCount: number; } /** * Orchestration state tracking */ export interface OrchestrationState { taskId: string; mode: ExecutionMode; iteration: number; currentPhase: LoopPhase; completedAgents: Set<string>; failedAgents: Set<string>; startTime: number; lastUpdateTime: number; } /** * Feedback for next iteration */ export interface IterationFeedback { gatePassRate?: number; consensusAverage?: number; previousFailures?: string[]; reasons?: string[]; timestamp?: number; } /** * Mode-specific configuration */ interface ModeThresholds { gateThreshold: number; consensusThreshold: number; maxIterations: number; } const MODE_CONFIG: Record<ExecutionMode, ModeThresholds> = { mvp: { gateThreshold: 0.70, consensusThreshold: 0.80, maxIterations: 5, }, standard: { gateThreshold: 0.95, consensusThreshold: 0.90, maxIterations: 10, }, enterprise: { gateThreshold: 0.98, consensusThreshold: 0.95, maxIterations: 15, }, }; /** * Shell escape utility for safe command execution */ function escapeShellArg(arg: string): string { // Use single quotes and escape any single quotes in the argument return `'${arg.replace(/'/g, "'\\''")}'`; } /** * Main orchestrator class */ export class Orchestrator { private config: OrchestrationConfig; private state: OrchestrationState; private testResults: Map<string, TestResult> = new Map(); private consensusScores: Map<string, number> = new Map(); private decision: ProductOwnerDecision = null; private errors: Map<string, Error> = new Map(); private phaseHistory: PhaseTransition[] = []; constructor(config: OrchestrationConfig) { // Validate configuration this.validateConfig(config); this.config = config; this.state = this.initializeState(config); } /** * Validate configuration parameters */ private validateConfig(config: OrchestrationConfig): void { if (!config.taskId || config.taskId.trim() === '') { throw new Error('Task ID cannot be empty'); } const validModes: ExecutionMode[] = ['mvp', 'standard', 'enterprise']; if (!validModes.includes(config.mode)) { throw new Error(`Invalid execution mode: ${config.mode}`); } if (!Number.isInteger(config.maxIterations) || config.maxIterations < 1) { throw new Error('Max iterations must be at least 1'); } if (config.maxIterations > 100) { throw new Error('Max iterations cannot exceed 100'); } // Validate timeout configuration if provided if (config.timeouts) { const MIN_TIMEOUT = 10; const MAX_TIMEOUT = 3600; if (config.timeouts.loop3Agent !== undefined) { if (config.timeouts.loop3Agent < MIN_TIMEOUT || config.timeouts.loop3Agent > MAX_TIMEOUT) { throw new Error(`loop3Agent timeout must be between ${MIN_TIMEOUT}-${MAX_TIMEOUT}s, got ${config.timeouts.loop3Agent}s`); } } if (config.timeouts.loop2Agent !== undefined) { if (config.timeouts.loop2Agent < MIN_TIMEOUT || config.timeouts.loop2Agent > MAX_TIMEOUT) { throw new Error(`loop2Agent timeout must be between ${MIN_TIMEOUT}-${MAX_TIMEOUT}s, got ${config.timeouts.loop2Agent}s`); } } if (config.timeouts.productOwner !== undefined) { if (config.timeouts.productOwner < MIN_TIMEOUT || config.timeouts.productOwner > MAX_TIMEOUT) { throw new Error(`productOwner timeout must be between ${MIN_TIMEOUT}-${MAX_TIMEOUT}s, got ${config.timeouts.productOwner}s`); } } } } /** * Initialize orchestration state */ private initializeState(config: OrchestrationConfig): OrchestrationState { const now = Date.now(); return { taskId: config.taskId, mode: config.mode, iteration: 0, currentPhase: 'loop3', completedAgents: new Set(), failedAgents: new Set(), startTime: now, lastUpdateTime: now, }; } /** * Get current orchestration state */ public getState(): OrchestrationState { return { ...this.state, completedAgents: new Set(this.state.completedAgents), failedAgents: new Set(this.state.failedAgents) }; } /** * Get task ID */ public getTaskId(): string { return this.config.taskId; } /** * Get execution mode */ public getMode(): ExecutionMode { return this.config.mode; } /** * Get maximum iterations for mode */ public getMaxIterations(): number { return this.config.maxIterations; } /** * Get gate threshold for current mode */ public getGateThreshold(): number { return MODE_CONFIG[this.config.mode].gateThreshold; } /** * Get consensus threshold for current mode */ public getConsensusThreshold(): number { return MODE_CONFIG[this.config.mode].consensusThreshold; } /** * Get timeout configuration with defaults * Defaults: Loop 3 = 300s, Loop 2 = 300s, Product Owner = 60s */ public getTimeouts(): { loop3Agent: number; loop2Agent: number; productOwner: number } { return { loop3Agent: this.config.timeouts?.loop3Agent ?? 300, loop2Agent: this.config.timeouts?.loop2Agent ?? 300, productOwner: this.config.timeouts?.productOwner ?? 60, }; } /** * Transition to next phase */ public transitionPhase(newPhase: LoopPhase): void { const transition: PhaseTransition = { fromPhase: this.state.currentPhase, toPhase: newPhase, timestamp: Date.now(), iteration: this.state.iteration, }; this.phaseHistory.push(transition); this.state.currentPhase = newPhase; this.state.lastUpdateTime = Date.now(); } /** * Increment iteration counter */ public incrementIteration(): void { this.state.iteration++; this.state.lastUpdateTime = Date.now(); } /** * Check if can continue iterating */ public canContinueIterating(): boolean { return this.state.iteration < this.config.maxIterations; } /** * Check if orchestration should terminate */ public shouldTerminate(): boolean { if (this.decision === 'PROCEED' || this.decision === 'ABORT') { return true; } if (this.decision === 'ITERATE' && !this.canContinueIterating()) { return true; } return false; } /** * Mark agent as completed */ public markAgentComplete(agentId: string, _loopType: 'loop3' | 'loop2'): void { this.state.completedAgents.add(agentId); this.state.failedAgents.delete(agentId); this.state.lastUpdateTime = Date.now(); } /** * Mark agent as failed */ public markAgentFailed(agentId: string, _loopType: 'loop3' | 'loop2'): void { this.state.failedAgents.add(agentId); this.state.completedAgents.delete(agentId); this.state.lastUpdateTime = Date.now(); } /** * Record execution error for agent */ public recordExecutionError(agentId: string, error: Error): void { this.errors.set(agentId, error); this.markAgentFailed(agentId, 'loop3'); } /** * Record timeout for agent */ public recordTimeout(agentId: string, timeoutSeconds: number): void { const error = new Error(`Agent timeout after ${timeoutSeconds}s`); this.recordExecutionError(agentId, error); } /** * Record test results for agent */ public recordTestResult(agentId: string, result: TestResult): void { this.testResults.set(agentId, result); this.state.lastUpdateTime = Date.now(); } /** * Get test result for agent */ public getTestResult(agentId: string): TestResult | undefined { return this.testResults.get(agentId); } /** * Aggregate test results across all agents */ public aggregateTestResults(): AggregatedTestResults { let totalPass = 0; let totalFail = 0; let totalSkip = 0; this.testResults.forEach((result) => { totalPass += result.pass; totalFail += result.fail; totalSkip += result.skip ?? 0; }); const total = totalPass + totalFail + totalSkip; const passRate = total === 0 ? 0 : totalPass / total; return { totalPass, totalFail, totalSkip, passRate, agentCount: this.testResults.size, }; } /** * Check gate (Loop 3 → Loop 2 transition) */ public checkGate(passRate: number): GateCheckResult { const threshold = this.getGateThreshold(); const params: GateCheckParams = { passRate, mode: this.config.mode, threshold, }; const result = gateCheck(params); return { passed: result.passed, passRate: result.passRate, threshold: result.threshold, gap: result.gap, }; } /** * Record consensus score from validator */ public recordConsensusScore(validatorId: string, score: number): void { if (score < 0 || score > 1) { throw new Error(`Invalid consensus score: ${score} (must be 0.0-1.0)`); } this.consensusScores.set(validatorId, score); this.state.lastUpdateTime = Date.now(); } /** * Get all consensus scores */ public getConsensusScores(): number[] { return Array.from(this.consensusScores.values()); } /** * Get consensus average */ public getConsensusAverage(): number { const scores = this.getConsensusScores(); if (scores.length === 0) { throw new Error('No consensus scores recorded'); } const sum = scores.reduce((a, b) => a + b, 0); return sum / scores.length; } /** * Validate consensus against threshold */ public validateConsensus(): ConsensusValidationResult { const scores = this.getConsensusScores(); if (scores.length === 0) { throw new Error('No consensus scores recorded'); } const result = collectConsensus(scores); const validation = validateConsensus({ average: result.average, mode: this.config.mode, threshold: this.getConsensusThreshold(), }); return { passed: validation.passed, average: validation.average, threshold: validation.threshold, gap: validation.gap, }; } /** * Record product owner decision */ public recordDecision(decision: ProductOwnerDecision): void { this.decision = decision; this.state.lastUpdateTime = Date.now(); } /** * Get recorded decision */ public getDecision(): ProductOwnerDecision { return this.decision; } /** * Parse decision from agent output */ public parseDecisionFromOutput(output: string): ProductOwnerDecision { const normalizedOutput = output.toUpperCase(); if (normalizedOutput.includes('PROCEED')) { return 'PROCEED'; } if (normalizedOutput.includes('ITERATE')) { return 'ITERATE'; } if (normalizedOutput.includes('ABORT')) { return 'ABORT'; } return null; } /** * Spawn Loop 3 (implementer) agents */ public async spawnLoop3Agents(agentTypes: string[]): Promise<AgentExecutionContext[]> { const agents: AgentExecutionContext[] = []; const now = Date.now(); agentTypes.forEach((agentType, index) => { agents.push({ agentId: `${agentType}-${this.state.iteration + 1}-${index + 1}`, agentType, loopType: 'loop3', iteration: this.state.iteration + 1, taskId: this.config.taskId, timestamp: now, }); }); return agents; } /** * Spawn Loop 2 (validator) agents */ public async spawnLoop2Validators(validatorTypes: string[]): Promise<AgentExecutionContext[]> { const validators: AgentExecutionContext[] = []; const now = Date.now(); validatorTypes.forEach((validatorType, index) => { validators.push({ agentId: `${validatorType}-${this.state.iteration + 1}-${index + 1}`, agentType: validatorType, loopType: 'loop2', iteration: this.state.iteration + 1, taskId: this.config.taskId, timestamp: now, }); }); return validators; } /** * Build task context string for agent spawning */ private buildTaskContext(): string { const context = { taskId: this.config.taskId, mode: this.config.mode, iteration: this.state.iteration, phase: this.state.currentPhase, timestamp: Date.now(), ...(this.config.workspace && { workspace: this.config.workspace }), ...(this.config.taskDescription && { taskDescription: this.config.taskDescription }), }; return JSON.stringify(context); } /** * Wait for agents to complete via Redis coordination * Blocks until all agents signal completion or timeout occurs * * @param spawnResults - Results from agent spawning * @param timeoutSeconds - Maximum wait time (default: 300s) * @returns Array of completed agent IDs */ private async waitForAgentsToComplete( spawnResults: SpawnResult[], timeoutSeconds: number = 300 ): Promise<string[]> { const completedAgents: string[] = []; const startTime = Date.now(); const projectRoot = process.env.PROJECT_ROOT || process.cwd(); console.log(`Waiting for ${spawnResults.length} agents to complete (timeout: ${timeoutSeconds}s)...`); for (const result of spawnResults) { if (!result.success) { console.warn(`Skipping failed agent: ${result.agentId}`); continue; } const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000); const remainingTimeout = timeoutSeconds - elapsedSeconds; if (remainingTimeout <= 0) { console.error(`Global timeout reached. Remaining agents will not be waited for.`); this.recordTimeout(result.agentId, timeoutSeconds); break; } try { // Agents push completion to legacy done list: swarm:{taskId}:{agentId}:done const redisHost = process.env.CFN_REDIS_HOST || process.env.REDIS_HOST || 'localhost'; const redisPort = process.env.CFN_REDIS_PORT || process.env.REDIS_PORT || '6379'; const doneListKey = `swarm:${this.config.taskId}:${result.agentId}:done`; const escapedHost = escapeShellArg(redisHost); const escapedPort = escapeShellArg(redisPort); const escapedDoneKey = escapeShellArg(doneListKey); // Use short blocking chunks to stay under tool time limits const chunkTimeout = Math.min(Math.max(remainingTimeout, 5), 60); // 5-60s console.log( `Waiting for agent ${result.agentId} via Redis BLPOP ${doneListKey} (chunk: ${chunkTimeout}s, remaining: ${remainingTimeout}s)...` ); // First a quick length check to avoid blocking if already complete try { const len = parseInt( execSync(`redis-cli -h ${escapedHost} -p ${escapedPort} LLEN ${escapedDoneKey}`, { encoding: 'utf8' }).trim(), 10 ); if (!Number.isNaN(len) && len > 0) { execSync(`redis-cli -h ${escapedHost} -p ${escapedPort} LPOP ${escapedDoneKey}`, { stdio: 'ignore' }); console.log(`✓ Agent ${result.agentId} completed (pre-existing done signal)`); completedAgents.push(result.agentId); this.markAgentComplete(result.agentId, 'loop3'); continue; } } catch { // ignore LLEN errors and continue to BLPOP } // Blocking wait chunk execSync(`redis-cli -h ${escapedHost} -p ${escapedPort} BLPOP ${escapedDoneKey} ${chunkTimeout}`, { stdio: 'ignore', timeout: chunkTimeout * 1000, cwd: projectRoot, }); console.log(`✓ Agent ${result.agentId} completed`); completedAgents.push(result.agentId); this.markAgentComplete(result.agentId, 'loop3'); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); console.error(`✗ Agent ${result.agentId} failed or timed out: ${errorMsg}`); this.recordExecutionError(result.agentId, new Error(errorMsg)); } } console.log(`Completed: ${completedAgents.length}/${spawnResults.length} agents`); return completedAgents; } /** * Collect agent outputs from Redis * Retrieves test results, confidence scores, and deliverables * * @param agentIds - List of agent IDs to collect from * @returns Map of agent outputs */ private async collectAgentOutputs( agentIds: string[] ): Promise<Map<string, { testResult?: TestResult; confidence?: number; deliverables?: string[] }>> { const outputs = new Map<string, { testResult?: TestResult; confidence?: number; deliverables?: string[] }>(); console.log(`Collecting outputs from ${agentIds.length} agents...`); for (const agentId of agentIds) { try { // Retrieve agent output from Redis const testResultJson = this.getRedisValue(`swarm:${this.config.taskId}:agent:${agentId}:test-result`); const confidenceStr = this.getRedisValue(`swarm:${this.config.taskId}:agent:${agentId}:confidence`); const deliverablesJson = this.getRedisValue(`swarm:${this.config.taskId}:agent:${agentId}:deliverables`); const agentOutput: { testResult?: TestResult; confidence?: number; deliverables?: string[] } = {}; // Parse test results if (testResultJson) { try { const testResult = JSON.parse(testResultJson) as TestResult; agentOutput.testResult = testResult; this.recordTestResult(agentId, testResult); console.log(` ${agentId}: Test results collected (${testResult.pass} pass, ${testResult.fail} fail)`); } catch (parseError) { console.warn(` ${agentId}: Failed to parse test results: ${parseError}`); } } // Parse confidence score if (confidenceStr) { const confidence = parseFloat(confidenceStr); if (!isNaN(confidence) && confidence >= 0 && confidence <= 1) { agentOutput.confidence = confidence; console.log(` ${agentId}: Confidence score: ${(confidence * 100).toFixed(2)}%`); } } // Parse deliverables if (deliverablesJson) { try { const deliverables = JSON.parse(deliverablesJson) as string[]; agentOutput.deliverables = deliverables; console.log(` ${agentId}: Deliverables: ${deliverables.length} files`); } catch (parseError) { console.warn(` ${agentId}: Failed to parse deliverables: ${parseError}`); } } outputs.set(agentId, agentOutput); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); console.error(` ${agentId}: Failed to collect output: ${errorMsg}`); } } console.log(`Successfully collected outputs from ${outputs.size}/${agentIds.length} agents`); return outputs; } /** * Get value from Redis using redis-cli * * @param key - Redis key * @returns Value or null if not found */ private getRedisValue(key: string): string | null { try { const redisHost = process.env.REDIS_HOST || 'localhost'; const redisPort = process.env.REDIS_PORT || '6379'; // Properly escape all user-controlled inputs to prevent shell injection const escapedHost = escapeShellArg(redisHost); const escapedPort = escapeShellArg(redisPort); const escapedKey = escapeShellArg(key); const result = execSync(`redis-cli -h ${escapedHost} -p ${escapedPort} GET ${escapedKey}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'], // Suppress stderr }).trim(); return result === '(nil)' ? null : result; } catch (error) { return null; } } /** * Execute tests against agent deliverables * Runs test suite to validate actual agent work * * @param agentOutputs - Map of agent outputs with deliverables * @returns Aggregated test results */ private async executeTestsOnDeliverables( agentOutputs: Map<string, { testResult?: TestResult; confidence?: number; deliverables?: string[] }> ): Promise<AggregatedTestResults> { console.log('Executing tests on agent deliverables...'); const projectRoot = process.env.PROJECT_ROOT || process.cwd(); // Validate TEST_COMMAND against allowlist to prevent shell injection (CVSS 8.5) const ALLOWED_TEST_COMMANDS = ['npm test', 'npm run test', 'jest', 'mocha', 'yarn test']; const ALLOWED_TEST_PATTERNS = [ /^npm run test:[a-z0-9-]+$/, // Namespaced npm scripts: npm run test:integration, test:security, etc. /^jest [a-z0-9/_-]+\.test\.[jt]s$/, // Jest with specific test files (no path traversal) /^mocha [a-z0-9/_-]+\.test\.[jt]s$/ // Mocha with specific test files (no path traversal) ]; const testCommand = process.env.TEST_COMMAND || 'npm test'; // Security: Block path traversal attempts if (testCommand.includes('..')) { throw new Error( `Security: Path traversal detected in TEST_COMMAND. Got: ${testCommand}` ); } // Check exact match first, then regex patterns const isAllowed = ALLOWED_TEST_COMMANDS.includes(testCommand) || ALLOWED_TEST_PATTERNS.some(pattern => pattern.test(testCommand)); if (!isAllowed) { throw new Error( `Security: Invalid TEST_COMMAND value. Allowed commands: ${ALLOWED_TEST_COMMANDS.join(', ')}, ` + `npm run test:*, jest <file>.test.[jt]s, mocha <file>.test.[jt]s. Got: ${testCommand}` ); } let totalPass = 0; let totalFail = 0; let totalSkip = 0; let agentCount = 0; for (const [agentId, output] of agentOutputs) { // Verify deliverables exist if (!output.deliverables || output.deliverables.length === 0) { console.warn(` ${agentId}: No deliverables to test`); continue; } // Validate deliverables exist on filesystem const missingFiles: string[] = []; for (const deliverable of output.deliverables) { const filePath = path.join(projectRoot, deliverable); try { await fs.access(filePath); } catch { missingFiles.push(deliverable); } } if (missingFiles.length > 0) { console.warn(` ${agentId}: Missing deliverables: ${missingFiles.join(', ')}`); const testResult: TestResult = { pass: 0, fail: missingFiles.length, skip: 0, }; this.recordTestResult(agentId, testResult); totalFail += missingFiles.length; agentCount++; continue; } // Execute test suite try { console.log(` ${agentId}: Running tests on ${output.deliverables.length} deliverables...`); const testOutput = execSync(testCommand, { encoding: 'utf8', cwd: projectRoot, stdio: 'pipe', }); // Parse test output (example for Jest format) const passMatch = testOutput.match(/(\d+) passing/); const failMatch = testOutput.match(/(\d+) failing/); const skipMatch = testOutput.match(/(\d+) pending/); const pass = passMatch && passMatch[1] ? parseInt(passMatch[1], 10) : 0; const fail = failMatch && failMatch[1] ? parseInt(failMatch[1], 10) : 0; const skip = skipMatch && skipMatch[1] ? parseInt(skipMatch[1], 10) : 0; const testResult: TestResult = { pass, fail, skip }; this.recordTestResult(agentId, testResult); totalPass += pass; totalFail += fail; totalSkip += skip; agentCount++; console.log(` ${agentId}: Tests completed (${pass} pass, ${fail} fail, ${skip} skip)`); } catch (error) { // Test execution failed const errorMsg = error instanceof Error ? error.message : String(error); console.error(` ${agentId}: Test execution failed: ${errorMsg}`); const testResult: TestResult = { pass: 0, fail: output.deliverables.length, skip: 0, }; this.recordTestResult(agentId, testResult); totalFail += output.deliverables.length; agentCount++; } } const total = totalPass + totalFail + totalSkip; const passRate = total === 0 ? 0 : totalPass / total; console.log(`Test execution complete: ${totalPass} pass, ${totalFail} fail, ${totalSkip} skip (${(passRate * 100).toFixed(2)}% pass rate)`); return { totalPass, totalFail, totalSkip, passRate, agentCount, }; } /** * Build agent context for spawning */ public buildAgentContext( agentId: string, loopType: 'loop3' | 'loop2', iteration: number, _feedback?: IterationFeedback ): AgentExecutionContext { return { agentId, agentType: 'unknown', loopType, iteration, taskId: this.config.taskId, timestamp: Date.now(), }; } /** * Prepare feedback for next iteration */ public prepareFeedback(feedback: IterationFeedback): IterationFeedback { return { ...feedback, timestamp: Date.now(), }; } /** * Get phase history */ public getPhaseHistory(): PhaseTransition[] { return [...this.phaseHistory]; } /** * Get execution errors */ public getErrors(): Map<string, Error> { return new Map(this.errors); } /** * Reset state for new iteration */ public resetForIteration(): void { this.testResults.clear(); this.consensusScores.clear(); this.decision = null; this.errors.clear(); this.state.completedAgents.clear(); this.state.failedAgents.clear(); } /** * Get orchestration summary */ public getSummary(): { taskId: string; mode: ExecutionMode; iteration: number; totalAgentsCompleted: number; totalAgentsFailed: number; decision: ProductOwnerDecision; duration: number; } { return { taskId: this.config.taskId, mode: this.config.mode, iteration: this.state.iteration, totalAgentsCompleted: this.state.completedAgents.size, totalAgentsFailed: this.state.failedAgents.size, decision: this.decision, duration: Date.now() - this.state.startTime, }; } /** * Execute the complete CFN Loop orchestration workflow * Runs iterations with Loop 3 → Loop 2 → Product Owner progression * Returns final decision (PROCEED/ITERATE/ABORT) */ public async execute(): Promise<ProductOwnerDecision> { const maxIterations = this.config.maxIterations; // Main iteration loop for (let iteration = 1; iteration <= maxIterations; iteration++) { this.incrementIteration(); console.log(`\n${'='.repeat(60)}`); console.log(`=== ITERATION ${iteration}/${maxIterations} ===`); console.log(`${'='.repeat(60)}`); // ===== LOOP 3: IMPLEMENTERS ===== console.log('\nPhase: Loop 3 (Implementers)'); this.transitionPhase('loop3'); const loop3AgentTypes = this.config.loop3Agents || ['backend-dev', 'coder']; const taskContext = this.buildTaskContext(); // Spawn real CLI agents console.log(`Spawning ${loop3AgentTypes.length} Loop 3 agents via CLI...`); const loop3SpawnResult = await spawnLoop3Agents( this.config.taskId, this.state.iteration, loop3AgentTypes, taskContext ); console.log(`Loop 3 spawn summary: ${loop3SpawnResult.successCount} successful, ${loop3SpawnResult.failureCount} failed`); // Wait for agents to complete via Redis coordination const timeouts = this.getTimeouts(); const completedAgentIds = await this.waitForAgentsToComplete( loop3SpawnResult.results, timeouts.loop3Agent ); if (completedAgentIds.length === 0) { console.error('No agents completed successfully. Aborting iteration.'); this.recordDecision('ABORT'); break; } // Collect agent outputs (test results, confidence scores, deliverables) const agentOutputs = await this.collectAgentOutputs(completedAgentIds); // Execute tests against actual agent deliverables const aggregated = await this.executeTestsOnDeliverables(agentOutputs); console.log( `Loop 3 Results: ${aggregated.totalPass} pass, ${aggregated.totalFail} fail (${aggregated.agentCount} agents, ${(aggregated.passRate * 100).toFixed(2)}% pass rate)` ); const gateResult = this.checkGate(aggregated.passRate); console.log(`Gate Check: ${gateResult.passed ? 'PASSED' : 'FAILED'} (threshold: ${(gateResult.threshold * 100).toFixed(2)}%)`); if (!gateResult.passed) { console.log(`Gate failed. Iterating...`); // Prepare feedback for next iteration this.prepareFeedback({ gatePassRate: aggregated.passRate, previousFailures: Array.from(this.state.failedAgents), reasons: [`Gate check failed: ${(gateResult.gap * 100).toFixed(2)}% below threshold`], }); console.log(`Feedback prepared for iteration ${iteration + 1}`); // Reset state for next iteration this.resetForIteration(); if (!this.canContinueIterating()) { console.log(`Max iterations (${maxIterations}) reached. ABORTING.`); this.recordDecision('ABORT'); break; } continue; // Go to next iteration } // ===== LOOP 2: VALIDATORS ===== console.log('\nPhase: Loop 2 (Validators)'); this.transitionPhase('loop2'); const loop2AgentTypes = this.config.loop2Agents || ['code-reviewer', 'tester', 'security-specialist']; // Spawn real CLI validators console.log(`Spawning ${loop2AgentTypes.length} Loop 2 validators via CLI...`); const loop2SpawnResult = await spawnLoop2Agents( this.config.taskId, this.state.iteration, loop2AgentTypes, taskContext ); console.log(`Loop 2 spawn summary: ${loop2SpawnResult.successCount} successful, ${loop2SpawnResult.failureCount} failed`); // Wait for validators to complete via Redis coordination const completedValidatorIds = await this.waitForAgentsToComplete( loop2SpawnResult.results, timeouts.loop2Agent ); if (completedValidatorIds.length === 0) { console.error('No validators completed successfully. Iterating...'); this.prepareFeedback({ reasons: ['No Loop 2 validators completed'], }); this.resetForIteration(); if (!this.canContinueIterating()) { console.log(`Max iterations (${maxIterations}) reached. ABORTING.`); this.recordDecision('ABORT'); break; } continue; } // Collect validator outputs (consensus scores) const validatorOutputs = await this.collectAgentOutputs(completedValidatorIds); // Record consensus scores from validators for (const [validatorId, output] of validatorOutputs) { if (output.confidence !== undefined) { this.recordConsensusScore(validatorId, output.confidence); } } console.log(`Loop 2 validators completed: ${completedValidatorIds.length}/${loop2SpawnResult.totalSpawned}`); // Validate consensus const consensusValidation = this.validateConsensus(); console.log( `Loop 2 Consensus: ${(consensusValidation.average * 100).toFixed(2)}% (threshold: ${(consensusValidation.threshold * 100).toFixed(2)}%)` ); if (!consensusValidation.passed) { console.log(`Consensus failed. Iterating...`); // Prepare feedback for next iteration this.prepareFeedback({ consensusAverage: consensusValidation.average, reasons: [`Consensus below threshold: ${(consensusValidation.gap * 100).toFixed(2)}%`], }); console.log(`Feedback prepared for iteration ${iteration + 1}`); // Reset state for next iteration this.resetForIteration(); if (!this.canContinueIterating()) { console.log(`Max iterations (${maxIterations}) reached. ABORTING.`); this.recordDecision('ABORT'); break; } continue; // Go to next iteration } // ===== PRODUCT OWNER DECISION ===== console.log('\nPhase: Product Owner Decision'); this.transitionPhase('product-owner'); const ownerAgent = this.config.productOwner || 'product-owner-agent'; console.log(`Consulting Product Owner (${ownerAgent})`); // Execute Product Owner decision via skill let decision: ProductOwnerDecision = 'PROCEED'; try { const projectRoot = path.resolve(__dirname, '../../../..'); const skillPath = path.join(projectRoot, '.claude/skills/cfn-product-owner-decision/execute-decision.sh'); const poAgentId = `product-owner-${this.config.taskId}-${iteration}`; const poArgs = [ '--task-id', this.config.taskId, '--agent-id', poAgentId, '--consensus', String(consensusValidation.average), '--threshold', String(consensusValidation.threshold), '--iteration', String(iteration), '--max-iterations', String(maxIterations), '--timeout', String(timeouts.productOwner), ]; if (this.config.successCriteriaEnabled) { poArgs.push('--success-criteria', 'enabled'); } console.log(`Executing Product Owner decision skill (timeout: ${timeouts.productOwner}s)...`); const escapedArgs = [escapeShellArg(skillPath), ...poArgs.map(arg => escapeShellArg(arg))].join(' '); const poOutput = execSync( `bash ${escapedArgs}`, { encoding: 'utf-8', timeout: (timeouts.productOwner + 10) * 1000 } ); // Parse decision from JSON output const jsonMatch = poOutput.match(/\{[\s\S]*"decision":\s*"(PROCEED|ITERATE|ABORT)"[\s\S]*\}/); if (jsonMatch) { const poResult = JSON.parse(jsonMatch[0]); decision = poResult.decision as ProductOwnerDecision; console.log(`Product Owner reasoning: ${poResult.reasoning}`); console.log(`Product Owner confidence: ${poResult.confidence}`); } else { // Fallback: try to extract decision from plain text const decisionMatch = poOutput.match(/Decision:\s*(PROCEED|ITERATE|ABORT)/i); if (decisionMatch && decisionMatch[1]) { decision = decisionMatch[1].toUpperCase() as ProductOwnerDecision; } else { console.warn('Could not parse Product Owner decision, defaulting to PROCEED'); decision = 'PROCEED'; } } } catch (error: unknown) { console.error(`Product Owner execution failed: ${error instanceof Error ? error.message : String(error)}`); console.warn('Defaulting to PROCEED due to execution error'); decision = 'PROCEED'; } this.recordDecision(decision); console.log(`Product Owner Decision: ${decision}`); // ===== DECISION HANDLING ===== if (decision === 'PROCEED') { console.log(`\n${'='.repeat(60)}`); console.log('SUCCESS: Product Owner approved. Orchestration complete.'); console.log(`${'='.repeat(60)}`); break; } else if (decision === 'ITERATE') { console.log(`Product Owner requested iteration. Preparing feedback for iteration ${iteration + 1}...`); // Prepare feedback for next iteration with Product Owner context const iterationFeedback = this.prepareFeedback({ gatePassRate: gateResult.passRate, consensusAverage: consensusValidation.average, reasons: [ `Product Owner requested iteration ${iteration + 1}`, `Gate pass rate: ${(gateResult.passRate * 100).toFixed(2)}%`, `Consensus: ${(consensusValidation.average * 100).toFixed(2)}%`, ], }); console.log('Feedback prepared:'); console.log(` - Gate: ${(iterationFeedback.gatePassRate! * 100).toFixed(2)}%`); console.log(` - Consensus: ${(iterationFeedback.consensusAverage! * 100).toFixed(2)}%`); console.log(` - Reasons: ${iterationFeedback.reasons?.join(', ')}`); // Store iteration feedback in Redis for next Loop 3 agents to access // Using proper escaping to prevent Redis command injection (CVSS 9.8) try { const feedbackKey = escapeShellArg(`swarm:${this.config.taskId}:iteration:${iteration + 1}:feedback`); const gatePassRateVal = escapeShellArg(String(iterationFeedback.gatePassRate)); const consensusAverageVal = escapeShellArg(String(iterationFeedback.consensusAverage)); const reasonsVal = escapeShellArg(iterationFeedback.reasons?.join('; ') || ''); const cmd = `redis-cli HSET ${feedbackKey} "gate_pass_rate" ${gatePassRateVal} "consensus_average" ${consensusAverageVal} "reasons" ${reasonsVal}`; execSync(cmd, { encoding: 'utf-8' }); console.log(`Iteration feedback stored in Redis for iteration ${iteration + 1}`); } catch (error: unknown) { console.warn(`Failed to store iteration feedback: ${error instanceof Error ? error.message : String(error)}`); } // Reset state for next iteration this.resetForIteration(); if (!this.canContinueIterating()) { console.log(`Max iterations (${maxIterations}) reached. ABORTING.`); this.recordDecision('ABORT'); break; } console.log(`\nProceeding to iteration ${iteration + 1}...`); continue; // Go to next iteration } else if (decision === 'ABORT') { console.log(`\n${'='.repeat(60)}`); console.log('FAILURE: Product Owner rejected. Aborting orchestration.'); console.log(`${'='.repeat(60)}`); break; } } // Final status const finalDecision = this.getDecision() || 'ABORT'; const summary = this.getSummary(); console.log(`\nFinal Summary:`); console.log(` Task ID: ${summary.taskId}`); console.log(` Mode: ${summary.mode}`); console.log(` Iterations: ${summary.iteration}/${this.config.maxIterations}`); console.log(` Completed Agents: ${this.state.completedAgents.size}`); console.log(` Failed Agents: ${this.state.failedAgents.size}`); console.log(` Decision: ${finalDecision}`); console.log(` Duration: ${(summary.duration / 1000).toFixed(2)}s`); return finalDecision; } } /** * CLI entry point for orchestrator */ if (require.main === module) { const args = process.argv.slice(2); // Parse command line arguments let taskId = ''; let mode: ExecutionMode = 'standard'; let maxIterations = 10; let loop3Agents: string[] = []; let loop2Agents: string[] = []; let productOwner = ''; let successCriteriaEnabled = false; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (!arg) continue; switch (arg) { case '--task-id': { const nextArg = args[++i]; if (nextArg) taskId = nextArg; break; } case '--mode': { const nextArg = args[++i]; if (nextArg) mode = nextArg as ExecutionMode; break; } case '--max-iterations': { const nextArg = args[++i]; if (nextArg) maxIterations = parseInt(nextArg, 10); break; } case '--loop3-agents': { const nextArg = args[++i]; if (nextArg) { loop3Agents = nextArg.split(',').map((a) => a.trim()).filter((a) => a.length > 0); } break; } case '--loop2-agents': { const nextArg = args[++i]; if (nextArg) { loop2Agents = nextArg.split(',').map((a) => a.trim()).filter((a) => a.length > 0); } break; } case '--product-owner': { const nextArg = args[++i]; if (nextArg) productOwner = nextArg; break; } case '--success-criteria': { const nextArg = args[++i]; if (nextArg) { successCriteriaEnabled = nextArg.toLowerCase() === 'enabled' || nextArg === 'true'; } break; } } } if (!taskId) { console.error('Error: --task-id is required'); process.exit(1); } const config: OrchestrationConfig = { taskId, mode, maxIterations, }; // Add optional parameters only if they have values if (loop3Agents.length > 0) { config.loop3Agents = loop3Agents; } if (loop2Agents.length > 0) { config.loop2Agents = loop2Agents; } if (productOwner) { config.productOwner = productOwner; } if (successCriteriaEnabled) { config.successCriteriaEnabled = successCriteriaEnabled; } const orchestrator = new Orchestrator(config); console.log(JSON.stringify(orchestrator.getState(), null, 2)); process.exit(0); } export default Orchestrator;