claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
531 lines (451 loc) • 16.6 kB
text/typescript
/**
* MDAP Local Orchestrator
*
* Simple Promise.all-based orchestrator that replaces complex Trigger.dev coordination.
* Uses parallel decomposition and implementation with configurable mode thresholds.
* Supports iteration loop with gate checks for validation.
*
* Extracted and simplified from cfn-orchestrator-v2.ts (1017 lines → ~200 lines)
*
* @module orchestrator
* @version 1.0.1
*/
import { spawn } from 'child_process';
import { promises as fs } from 'fs';
import * as path from 'path';
import {
decomposeArchitecture,
decomposeTesting,
decomposePerformance,
decomposeSecurity,
} from './decomposers/index.js';
import {
implement,
type ImplementerPayload,
type ImplementerResult,
} from './implementer.js';
import {
type MDAPResult,
type CompilerError,
} from './types.js';
// =============================================
// Type Definitions
// =============================================
export interface OrchestratorPayload {
/** Description of the task to be implemented */
taskDescription: string;
/** Working directory for implementation */
workDir: string;
/** Execution mode: mvp (fast), standard (production), enterprise (compliance) */
mode: 'mvp' | 'standard' | 'enterprise';
/** Override max iterations (default from mode config) */
maxIterations?: number;
/** Test command to execute (default: "npm test") */
testCommand?: string;
/** Target files to create/modify (optional - auto-detected if not provided) */
targetFiles?: string[];
}
export interface OrchestratorResult extends MDAPResult {
/** Number of iterations completed */
iterations: number;
/** Final gate check pass rate */
passRate?: number;
/** Total files processed */
filesProcessed: number;
/** Implementation results from each decomposer */
implementationResults: ImplementerResult[];
}
export interface ModeConfig {
/** Pass rate threshold for gate check */
gateThreshold: number;
/** Maximum iterations before abort */
maxIterations: number;
}
// =============================================
// Security: Input Validation
// =============================================
/**
* Allowed test commands (whitelist for security)
*/
const ALLOWED_TEST_COMMANDS = [
'npm test',
'npm run test',
'npm run test:unit',
'npm run test:integration',
'npm run test:e2e',
'yarn test',
'yarn test:unit',
'yarn test:integration',
'yarn test:e2e',
'pytest',
'go test',
'cargo test',
'make test',
];
/**
* Sanitize and validate work directory path
*/
function validateWorkDir(workDir: string): string {
// Resolve absolute path
const resolved = path.resolve(workDir);
// Basic path validation
if (!resolved || resolved.length < 2) {
throw new Error('Invalid work directory path');
}
// Prevent directory traversal
if (resolved.includes('..')) {
throw new Error('Directory traversal not allowed in work directory');
}
// Ensure path doesn't contain shell metacharacters
if (/[;&|`$(){}[\]]/.test(resolved)) {
throw new Error('Invalid characters in work directory path');
}
return resolved;
}
/**
* Validate and sanitize task description
*/
function validateTaskDescription(description: string): string {
if (!description || typeof description !== 'string') {
throw new Error('Task description is required and must be a string');
}
// Trim whitespace
const cleaned = description.trim();
// Check reasonable length
if (cleaned.length < 10 || cleaned.length > 10000) {
throw new Error('Task description must be between 10 and 10000 characters');
}
// Remove potential shell metacharacters
const sanitized = cleaned.replace(/[;&|`$(){}[\]<>"'\\]/g, '');
if (sanitized.length === 0) {
throw new Error('Task description contains only invalid characters');
}
return sanitized;
}
/**
* Validate test command against whitelist
*/
function validateTestCommand(command: string): string {
if (!command || typeof command !== 'string') {
return 'npm test'; // Default safe command
}
const trimmed = command.trim();
// Check against whitelist
if (!ALLOWED_TEST_COMMANDS.includes(trimmed)) {
console.warn(`Test command "${trimmed}" not in whitelist, using default "npm test"`);
return 'npm test';
}
return trimmed;
}
/**
* Safely execute a command without shell injection
*/
async function executeCommandSafely(command: string, workDir: string): Promise<{ stdout: string; stderr: string; exitCode: number }> {
return new Promise((resolve) => {
// Split command into parts (simple, no pipes or redirects)
const parts = command.split(' ');
const cmd = parts[0];
const args = parts.slice(1);
// Validate command is in whitelist
const fullCommand = parts.join(' ');
if (!ALLOWED_TEST_COMMANDS.includes(fullCommand)) {
resolve({
stdout: '',
stderr: 'Command not in whitelist',
exitCode: 1
});
return;
}
const child = spawn(cmd, args, {
cwd: workDir,
stdio: 'pipe',
shell: false, // Critical: Don't use shell to prevent injection
});
let stdout = '';
let stderr = '';
child.stdout?.on('data', (data) => {
stdout += data.toString();
});
child.stderr?.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
resolve({
stdout,
stderr,
exitCode: code ?? 0
});
});
child.on('error', (error) => {
resolve({
stdout: '',
stderr: error.message,
exitCode: 1
});
});
});
}
// =============================================
// Mode Configuration
// =============================================
const MODE_CONFIGS: Record<string, ModeConfig> = {
mvp: {
gateThreshold: 0.70,
maxIterations: 5,
},
standard: {
gateThreshold: 0.95,
maxIterations: 10,
},
enterprise: {
gateThreshold: 0.98,
maxIterations: 15,
},
};
// =============================================
// Gate Check Implementation (Secure)
// =============================================
/**
* Run gate check by safely executing test command
*
* @param testCommand - Command to execute for validation
* @param workDir - Working directory for command execution
* @returns Pass rate (0.0 to 1.0)
*/
async function runGateCheck(testCommand: string, workDir: string): Promise<number> {
try {
// Validate inputs
const safeCommand = validateTestCommand(testCommand);
const safeWorkDir = validateWorkDir(workDir);
// Execute command safely
const result = await executeCommandSafely(safeCommand, safeWorkDir);
// Parse test output to calculate pass rate
const output = result.stdout + result.stderr;
// Look for patterns like "X passing, Y failing" or "✓ X, ✗ Y"
const passingMatch = output.match(/(\d+)\s+passing|✓\s*(\d+)/);
const failingMatch = output.match(/(\d+)\s+failing|✗\s*(\d+)/);
const passing = passingMatch ? parseInt(passingMatch[1] || passingMatch[2]) : 0;
const failing = failingMatch ? parseInt(failingMatch[1] || failingMatch[2]) : 0;
const total = passing + failing;
if (total === 0) {
// No test results found - assume success if command didn't fail
return result.exitCode === 0 ? 1.0 : 0.0;
}
return passing / total;
} catch (error: any) {
// Command failed - check if it's test failure or error
console.error(`Gate check command failed: ${error.message}`);
return 0.0;
}
}
// =============================================
// Orchestrator Core
// =============================================
/**
* Auto-detect target files based on task description and work directory
*
* @param taskDescription - Task description to analyze
* @param workDir - Working directory to scan
* @returns Array of target file paths
*/
async function detectTargetFiles(taskDescription: string, workDir: string): Promise<string[]> {
const files: string[] = [];
// Validate inputs
const safeWorkDir = validateWorkDir(workDir);
// Simple heuristic - look for common file patterns
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs', '.java'];
try {
const entries = await fs.readdir(safeWorkDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile()) {
const ext = path.extname(entry.name);
if (extensions.includes(ext)) {
// Check if file relates to task description
const filePath = path.join(safeWorkDir, entry.name);
const content = await fs.readFile(filePath, 'utf8');
// Simple keyword matching
const keywords = taskDescription.toLowerCase().split(/\s+/);
const matchedKeywords = keywords.filter(keyword =>
keyword.length > 3 && content.toLowerCase().includes(keyword)
);
if (matchedKeywords.length > 0) {
files.push(filePath);
}
}
}
}
} catch (error) {
console.warn('Could not scan directory for target files:', error);
}
// If no files detected, create a default based on task description
if (files.length === 0) {
const defaultFile = 'implementation.ts';
files.push(path.join(safeWorkDir, defaultFile));
}
return files;
}
/**
* Main orchestrator function
*
* @param payload - Orchestrator configuration
* @returns Orchestrator execution result
*/
export async function orchestrate(payload: OrchestratorPayload): Promise<OrchestratorResult> {
const startTime = Date.now();
// Validate all inputs
if (!payload || typeof payload !== 'object') {
throw new Error('Invalid payload: must be an object');
}
const safeTaskDescription = validateTaskDescription(payload.taskDescription);
const safeWorkDir = validateWorkDir(payload.workDir);
const safeTestCommand = validateTestCommand(payload.testCommand || 'npm test');
const modeConfig = MODE_CONFIGS[payload.mode];
if (!modeConfig) {
throw new Error(`Invalid mode: ${payload.mode}. Must be one of: ${Object.keys(MODE_CONFIGS).join(', ')}`);
}
const maxIterations = payload.maxIterations ?? modeConfig.maxIterations;
console.log(`Starting MDAP orchestration in ${payload.mode} mode`);
console.log(`Task: ${safeTaskDescription.substring(0, 100)}...`);
console.log(`Work Dir: ${safeWorkDir}`);
console.log(`Max Iterations: ${maxIterations}`);
console.log(`Gate Threshold: ${(modeConfig.gateThreshold * 100).toFixed(0)}%`);
// Auto-detect target files if not provided
const targetFiles = payload.targetFiles ??
await detectTargetFiles(safeTaskDescription, safeWorkDir);
console.log(`Target Files: ${targetFiles.join(', ')}`);
let iterations = 0;
let passRate = 0.0;
let implementationResults: ImplementerResult[] = [];
// =============================================
// Iteration Loop
// =============================================
for (iterations = 1; iterations <= maxIterations; iterations++) {
console.log(`\n=== Iteration ${iterations}/${maxIterations} ===`);
try {
// Step 1: Parallel Decomposition
console.log('Step 1: Running parallel decomposition...');
const decompositionResults = await Promise.all([
decomposeArchitecture({
taskId: `${payload.mode}-${iterations}-architecture`,
taskDescription: safeTaskDescription,
workDir: safeWorkDir,
}),
decomposeTesting({
taskId: `${payload.mode}-${iterations}-testing`,
taskDescription: safeTaskDescription,
workDir: safeWorkDir,
}),
decomposePerformance({
taskId: `${payload.mode}-${iterations}-performance`,
taskDescription: safeTaskDescription,
workDir: safeWorkDir,
}),
decomposeSecurity({
taskId: `${payload.mode}-${iterations}-security`,
taskDescription: safeTaskDescription,
workDir: safeWorkDir,
}),
]);
console.log(`Decomposition complete: ${decompositionResults.length} perspectives`);
// Step 2: Create implementation payloads
const implementationPayloads: ImplementerPayload[] = [];
for (const analysis of decompositionResults) {
// Use microTasks from the analysis
const microTasks = (analysis as any).microTasks || [];
for (const task of microTasks) {
const implPayload: ImplementerPayload = {
taskId: `${payload.mode}-${iterations}-${task.id}`,
taskDescription: task.description || safeTaskDescription,
targetFile: targetFiles[0], // Use first target file for all microtasks
workDir: safeWorkDir,
contextHints: [analysis.perspective, task.title || 'component'],
language: detectLanguage(safeTaskDescription),
};
implementationPayloads.push(implPayload);
}
}
// Ensure we have at least one payload
if (implementationPayloads.length === 0) {
implementationPayloads.push({
taskId: `${payload.mode}-${iterations}-default`,
taskDescription: safeTaskDescription,
targetFile: targetFiles[0],
workDir: safeWorkDir,
language: detectLanguage(safeTaskDescription),
});
}
// Step 3: Parallel Implementation
console.log(`Step 2: Implementing ${implementationPayloads.length} components...`);
const iterationResults = await Promise.all(
implementationPayloads.map(p => implement(p))
);
implementationResults.push(...iterationResults);
const successfulImplementations = iterationResults.filter(r => r.success);
console.log(`Implementation complete: ${successfulImplementations.length}/${iterationResults.length} successful`);
// Step 4: Gate Check (now async)
console.log('Step 3: Running gate check...');
passRate = await runGateCheck(safeTestCommand, safeWorkDir);
console.log(`Gate check result: ${(passRate * 100).toFixed(1)}% pass rate`);
// Step 5: Check Threshold
if (passRate >= modeConfig.gateThreshold) {
console.log(`✅ Gate passed! ${(passRate * 100).toFixed(1)}% >= ${(modeConfig.gateThreshold * 100).toFixed(0)}%`);
break;
} else {
console.log(`⚠️ Gate failed. ${(passRate * 100).toFixed(1)}% < ${(modeConfig.gateThreshold * 100).toFixed(0)}%`);
if (iterations === maxIterations) {
console.log(`Reached max iterations (${maxIterations}) - stopping`);
}
}
} catch (error) {
console.error(`Iteration ${iterations} failed:`, error);
passRate = 0.0;
}
}
// =============================================
// Final Result
// =============================================
const duration = Date.now() - startTime;
const success = passRate >= modeConfig.gateThreshold;
const filesProcessed = implementationResults.filter(r => r.success).length;
console.log(`\n=== Orchestration Complete ===`);
console.log(`Success: ${success}`);
console.log(`Iterations: ${iterations}`);
console.log(`Final Pass Rate: ${(passRate * 100).toFixed(1)}%`);
console.log(`Files Processed: ${filesProcessed}`);
console.log(`Duration: ${duration}ms`);
return {
success,
iterations,
passRate,
filesProcessed,
implementationResults,
durationMs: duration,
confidence: passRate >= modeConfig.gateThreshold ? 0.9 : 0.3,
};
}
// =============================================
// Utility Functions
// =============================================
/**
* Detect programming language from task description
*
* @param taskDescription - Task description to analyze
* @returns Language string
*/
function detectLanguage(taskDescription: string): string {
const description = taskDescription.toLowerCase();
if (description.includes('typescript') || description.includes('react')) {
return 'typescript';
} else if (description.includes('python')) {
return 'python';
} else if (description.includes('rust')) {
return 'rust';
} else if (description.includes('go')) {
return 'go';
} else if (description.includes('java')) {
return 'java';
} else if (description.includes('javascript') || description.includes('js')) {
return 'javascript';
}
return 'typescript'; // Default
}