aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
463 lines (406 loc) • 15 kB
JavaScript
/**
* External Ralph Loop CLI
*
* Entry point for the external Ralph supervisor that provides
* crash-resilient, long-running iterative task execution.
*
* Usage:
* node index.mjs "objective" --completion "criteria" [options]
* node index.mjs --resume [--max-iterations N]
* node index.mjs --status
* node index.mjs --abort
*
* @implements @.aiwg/requirements/design-ralph-external.md
*/
import { resolve } from 'path';
import { createWriteStream } from 'fs';
import { Orchestrator } from './orchestrator.mjs';
import { StateManager } from './state-manager.mjs';
import { isClaudeAvailable, getClaudeVersion } from './session-launcher.mjs';
import { createProvider, hasProvider, ensureProvidersRegistered } from './lib/provider-adapter.mjs';
import { MemoryManager } from './memory-manager.mjs';
import { BestOutputTracker } from './best-output-tracker.mjs';
import { IterationAnalytics } from './iteration-analytics.mjs';
import { EarlyStopping } from './early-stopping.mjs';
import { CrossTaskLearner } from './cross-task-learner.mjs';
/**
* Parse command line arguments
* @param {string[]} args
* @returns {Object}
*/
function parseArgs(args) {
const options = {
objective: null,
completionCriteria: null,
maxIterations: 5,
model: 'opus',
budgetPerIteration: 2.0,
timeoutMinutes: 60,
mcpConfig: null,
giteaIssue: false,
resume: false,
status: false,
abort: false,
help: false,
// New research-backed options (#149, #154, #167, #168, #170)
memory: 3, // Memory capacity Ω (#170)
crossTask: true, // Cross-task learning (#154)
enableAnalytics: true, // Iteration analytics (#167)
enableBestOutput: true, // Best output tracking (#168)
enableEarlyStopping: true, // Early stopping (#149)
provider: 'claude', // CLI provider (claude, codex, opencode, factory)
verbose: false, // Verbose per-iteration detail
logFile: null, // Optional log file path
};
let i = 0;
while (i < args.length) {
const arg = args[i];
if (arg === '--help' || arg === '-h') {
options.help = true;
} else if (arg === '--resume' || arg === '-r') {
options.resume = true;
} else if (arg === '--status' || arg === '-s') {
options.status = true;
} else if (arg === '--abort') {
options.abort = true;
} else if (arg === '--completion' || arg === '-c') {
options.completionCriteria = args[++i];
} else if (arg === '--max-iterations') {
options.maxIterations = parseInt(args[++i], 10);
} else if (arg === '--model') {
options.model = args[++i];
} else if (arg === '--budget') {
options.budgetPerIteration = parseFloat(args[++i]);
} else if (arg === '--timeout') {
options.timeoutMinutes = parseInt(args[++i], 10);
} else if (arg === '--mcp-config') {
options.mcpConfig = JSON.parse(args[++i]);
} else if (arg === '--gitea-issue') {
options.giteaIssue = true;
} else if (arg === '--memory' || arg === '-m') {
const memArg = args[++i];
options.memory = isNaN(parseInt(memArg)) ? memArg : parseInt(memArg, 10);
} else if (arg === '--cross-task') {
options.crossTask = true;
} else if (arg === '--no-cross-task') {
options.crossTask = false;
} else if (arg === '--no-analytics') {
options.enableAnalytics = false;
} else if (arg === '--no-best-output') {
options.enableBestOutput = false;
} else if (arg === '--no-early-stopping') {
options.enableEarlyStopping = false;
} else if (arg === '--provider') {
options.provider = args[++i];
} else if (arg === '--verbose' || arg === '-v') {
options.verbose = true;
} else if (arg === '--log-file') {
options.logFile = args[++i];
} else if (!arg.startsWith('-') && !options.objective) {
options.objective = arg;
}
i++;
}
return options;
}
/**
* Tee all console output to a log file with timestamps.
* Returns a cleanup function that closes the stream.
* @param {string} logFilePath
* @returns {() => void}
*/
function installConsoleTee(logFilePath) {
const stream = createWriteStream(logFilePath, { flags: 'a' });
function writeLine(level, args) {
const ts = new Date().toISOString();
const msg = args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ');
stream.write(`${ts} [${level}] ${msg}\n`);
}
const origLog = console.log.bind(console);
const origError = console.error.bind(console);
const origWarn = console.warn.bind(console);
console.log = (...args) => { origLog(...args); writeLine('LOG', args); };
console.error = (...args) => { origError(...args); writeLine('ERR', args); };
console.warn = (...args) => { origWarn(...args); writeLine('WRN', args); };
return () => stream.end();
}
/**
* Print help message
*/
function printHelp() {
console.log(`
External Ralph Loop - Crash-resilient iterative task execution
================================================================================
SECURITY WARNING
================================================================================
This tool spawns Claude Code with --dangerously-skip-permissions, which
BYPASSES ALL PERMISSION PROMPTS. Sessions can:
- Read/write ANY file without confirmation
- Execute ANY shell command without approval
- Run for hours without human oversight
BEFORE USING: Read docs/ralph-external-security.md
REQUIRED PRECAUTIONS:
- Clean git state (for rollback)
- Set budget limits (--budget)
- Set iteration limits (--max-iterations)
- Monitor session progress
- Be ready to abort if needed
================================================================================
USAGE:
ralph-external "<objective>" --completion "<criteria>" [options]
ralph-external --resume [options]
ralph-external --status
ralph-external --abort
ARGUMENTS:
<objective> Task objective (required for new loop)
OPTIONS:
-c, --completion <str> Completion criteria (required for new loop)
--max-iterations <n> Maximum external iterations (default: 5)
--model <model> Claude model (default: opus)
--budget <usd> Budget per iteration in USD (default: 2.0)
--timeout <min> Timeout per iteration in minutes (default: 60)
--mcp-config <json> MCP server configuration JSON
--gitea-issue Create/link Gitea issue for tracking
--provider <name> CLI provider: claude (default), codex, opencode, factory
RESEARCH-BACKED OPTIONS (REF-015, REF-021):
-m, --memory <n|preset> Memory capacity Ω: 1-10 or preset name
Presets: simple(1), moderate(3), complex(5), maximum(10)
Default: 3 (moderate)
--cross-task Enable cross-task learning (default: true)
--no-cross-task Disable cross-task learning
--no-analytics Disable iteration analytics
--no-best-output Disable best output selection (use final)
--no-early-stopping Disable early stopping on high confidence
-v, --verbose Enable verbose per-iteration detail (assessment,
strategy, prompt preview)
--log-file <path> Write timestamped log to file (in addition to stdout)
COMMANDS:
-r, --resume Resume interrupted loop
-s, --status Show current loop status
--abort Abort current loop
-h, --help Show this help message
EXAMPLES:
# Start new loop
ralph-external "Fix all failing tests" --completion "npm test passes"
# With options
ralph-external "Migrate to TypeScript" \\
--completion "npx tsc --noEmit exits 0" \\
--max-iterations 10 \\
--budget 5.0
# Resume interrupted loop
ralph-external --resume --max-iterations 15
# Check status
ralph-external --status
`);
}
/**
* Print status
* @param {string} projectRoot
*/
function printStatus(projectRoot) {
const stateManager = new StateManager(projectRoot);
const state = stateManager.load();
if (!state) {
console.log('No external Ralph loop found.');
return;
}
console.log(`
External Ralph Loop Status
==========================
Loop ID: ${state.loopId}
Status: ${state.status}
Objective: ${state.objective}
Criteria: ${state.completionCriteria}
Progress: ${state.currentIteration}/${state.maxIterations} iterations
Start Time: ${state.startTime}
Last Update: ${state.lastUpdate}
Iterations:
${state.iterations.map((iter, idx) => {
const status = iter.status || 'unknown';
const progress = iter.analysis?.completionPercentage || 0;
return ` ${idx + 1}. ${status} (${progress}% progress)`;
}).join('\n') || ' None yet'}
Learnings:
${state.accumulatedLearnings ? state.accumulatedLearnings.slice(0, 500) + '...' : ' None yet'}
`);
}
/**
* Main entry point
*/
async function main() {
const args = process.argv.slice(2);
const options = parseArgs(args);
const projectRoot = process.cwd();
// Install log file tee before any other output
let cleanupLogTee = null;
if (options.logFile) {
cleanupLogTee = installConsoleTee(options.logFile);
console.log(`[External Ralph] Log file: ${options.logFile}`);
}
// Handle help
if (options.help) {
printHelp();
process.exit(0);
}
// Handle status
if (options.status) {
printStatus(projectRoot);
process.exit(0);
}
// Handle abort
if (options.abort) {
const stateManager = new StateManager(projectRoot);
stateManager.clear();
console.log('External Ralph loop aborted.');
process.exit(0);
}
// Check provider availability
await ensureProvidersRegistered();
const providerName = options.provider || 'claude';
if (!hasProvider(providerName)) {
console.error(`Error: Unknown provider '${providerName}'. Available: claude, codex, opencode, factory`);
process.exit(1);
}
const adapter = createProvider(providerName);
const providerAvailable = await adapter.isAvailable();
if (!providerAvailable) {
console.error(`Error: ${providerName} CLI not found. Please install it.`);
process.exit(1);
}
const version = await adapter.getVersion();
console.log(`${providerName} CLI version: ${version}`);
const orchestrator = new Orchestrator(projectRoot);
// Track if shutdown is in progress to prevent double-handling
let shutdownInProgress = false;
// Handle signals for graceful shutdown
process.on('SIGINT', async () => {
if (shutdownInProgress) {
console.log('\n[External Ralph] Force quit requested');
process.exit(1);
}
shutdownInProgress = true;
console.log('\n[External Ralph] Received SIGINT, initiating graceful shutdown...');
try {
await orchestrator.gracefulShutdown();
} catch (error) {
console.error(`[External Ralph] Shutdown error: ${error.message}`);
}
process.exit(0);
});
process.on('SIGTERM', async () => {
if (shutdownInProgress) {
return;
}
shutdownInProgress = true;
console.log('\n[External Ralph] Received SIGTERM, initiating graceful shutdown...');
try {
await orchestrator.gracefulShutdown();
} catch (error) {
console.error(`[External Ralph] Shutdown error: ${error.message}`);
}
process.exit(0);
});
try {
let result;
if (options.resume) {
// Resume existing loop
result = await orchestrator.resume({
maxIterations: options.maxIterations,
budgetPerIteration: options.budgetPerIteration,
});
} else {
// Start new loop
if (!options.objective) {
console.error('Error: Objective is required. Use --help for usage.');
process.exit(1);
}
if (!options.completionCriteria) {
console.error('Error: Completion criteria is required. Use --completion.');
process.exit(1);
}
result = await orchestrator.execute({
objective: options.objective,
completionCriteria: options.completionCriteria,
maxIterations: options.maxIterations,
model: options.model,
budgetPerIteration: options.budgetPerIteration,
timeoutMinutes: options.timeoutMinutes,
mcpConfig: options.mcpConfig,
giteaIntegration: options.giteaIssue ? { enabled: true } : null,
provider: options.provider,
verbose: options.verbose,
});
}
// Print result
console.log(`\n[External Ralph] Loop completed:`);
console.log(` Success: ${result.success}`);
console.log(` Reason: ${result.reason}`);
console.log(` Iterations: ${result.iterations}`);
console.log(` Loop ID: ${result.loopId}`);
if (cleanupLogTee) cleanupLogTee();
process.exit(result.success ? 0 : 1);
} catch (error) {
console.error(`[External Ralph] Fatal error: ${error.message}`);
if (cleanupLogTee) cleanupLogTee();
process.exit(1);
}
}
// Run if executed directly
main().catch(console.error);
// Import process reliability modules
import { ProcessMonitor } from './process-monitor.mjs';
import { RecoveryEngine } from './recovery-engine.mjs';
// Import Epic #26 modules
import { PIDController } from './pid-controller.mjs';
import { GainScheduler } from './gain-scheduler.mjs';
import { MetricsCollector } from './metrics-collector.mjs';
import { ControlAlarms } from './control-alarms.mjs';
import { ClaudePromptGenerator } from './lib/claude-prompt-generator.mjs';
import { ValidationAgent } from './lib/validation-agent.mjs';
import { StrategyPlanner } from './lib/strategy-planner.mjs';
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';
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';
// Export all modules for programmatic use
export {
parseArgs,
printHelp,
printStatus,
// Core modules
Orchestrator,
StateManager,
// Research-backed modules (#149, #154, #167, #168, #170)
MemoryManager,
BestOutputTracker,
IterationAnalytics,
EarlyStopping,
CrossTaskLearner,
// Process reliability modules (Phase 4)
ProcessMonitor,
RecoveryEngine,
// Epic #26 - PID Control Layer (#23)
PIDController,
GainScheduler,
MetricsCollector,
ControlAlarms,
// Epic #26 - Claude Intelligence Layer (#22)
ClaudePromptGenerator,
ValidationAgent,
StrategyPlanner,
// Epic #26 - Memory Layer (#24)
SemanticMemory,
MemoryPromotion,
LearningExtractor,
MemoryRetrieval,
// Epic #26 - Overseer Layer (#25)
Overseer,
BehaviorDetector,
InterventionSystem,
EscalationHandler,
};