UNPKG

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

498 lines (490 loc) â€ĸ 18.1 kB
/** * Ralph Command Handlers * * CLI command handlers for Ralph iterative execution loop. * Uses external Ralph supervisor for crash-resilient background execution. * Falls back to internal scripts when --internal flag is used. * * @implements @.aiwg/architecture/decisions/ADR-001-unified-extension-system.md * @implements @.aiwg/working/issue-ralph-external-completion.md * @tests @test/unit/cli/handlers/ralph.test.ts * @issue #33, #275 */ import { createScriptRunner } from './script-runner.js'; import { launchExternalRalph, getLoopStatuses, abortLoop, resumeLoop, attachToLoopOutput, } from './ralph-launcher.js'; import { handlerResultFromError } from '../errors.js'; /** * Parse Ralph command arguments */ function parseRalphArgs(args) { const result = {}; let i = 0; while (i < args.length) { const arg = args[i]; if (arg === '--help' || arg === '-h') { result.help = true; } else if (arg === '--internal') { result.internal = true; } else if (arg === '--completion' || arg === '-c') { result.completionCriteria = args[++i]; } else if (arg === '--max-iterations') { result.maxIterations = parseInt(args[++i], 10); } else if (arg === '--model') { result.model = args[++i]; } else if (arg === '--budget') { result.budget = parseFloat(args[++i]); } else if (arg === '--timeout') { result.timeout = parseInt(args[++i], 10); } else if (arg === '--mcp-config') { result.mcpConfig = args[++i]; } else if (arg === '--gitea-issue') { result.giteaIssue = true; } else if (arg === '--memory' || arg === '-m') { const memArg = args[++i]; result.memory = isNaN(parseInt(memArg)) ? memArg : parseInt(memArg, 10); } else if (arg === '--cross-task') { result.crossTask = true; } else if (arg === '--no-cross-task') { result.crossTask = false; } else if (arg === '--no-analytics') { result.enableAnalytics = false; } else if (arg === '--no-best-output') { result.enableBestOutput = false; } else if (arg === '--no-early-stopping') { result.enableEarlyStopping = false; } else if (arg === '--loop-id') { result.loopId = args[++i]; } else if (arg === '--provider') { result.provider = args[++i]; } else if (arg === '--dangerous') { result.dangerous = true; } else if (arg === '--params') { result.params = args[++i]; } else if (arg === '--attach') { result.attach = true; } else if (arg === '--verbose' || arg === '-v') { result.verbose = true; } else if (arg === '--log-file') { result.logFile = args[++i]; } else if (!arg.startsWith('-') && !result.objective) { result.objective = arg; } i++; } return result; } /** * Agent Loop Handler * * Executes iterative task loops with automatic completion detection. * By default, launches as a detached background process that survives terminal closure. * Use --internal flag for the legacy foreground execution. */ export class RalphHandler { id = 'ralph'; name = 'Agent Loop'; description = 'Execute iterative task loop with automatic completion detection'; category = 'ralph'; aliases = ['ralph', '-ralph', '--ralph']; async execute(ctx) { const parsed = parseRalphArgs(ctx.args); // Show help if (parsed.help) { return { exitCode: 0, message: this.getHelpText(), }; } // Use internal (foreground) mode if requested if (parsed.internal) { const runner = createScriptRunner(ctx.frameworkRoot); return runner.run('tools/ralph/ralph-cli.mjs', ctx.args.filter((a) => a !== '--internal')); } // Validate required arguments if (!parsed.objective) { return { exitCode: 1, message: 'Error: Objective is required.\n\nUsage: aiwg ralph "<objective>" --completion "<criteria>"', }; } if (!parsed.completionCriteria) { return { exitCode: 1, message: 'Error: Completion criteria is required.\n\nUsage: aiwg ralph "<objective>" --completion "<criteria>"', }; } // Launch external Ralph as detached background process try { const options = { objective: parsed.objective, completionCriteria: parsed.completionCriteria, maxIterations: parsed.maxIterations, model: parsed.model, budget: parsed.budget, timeout: parsed.timeout, mcpConfig: parsed.mcpConfig, giteaIssue: parsed.giteaIssue, memory: parsed.memory, crossTask: parsed.crossTask, enableAnalytics: parsed.enableAnalytics, enableBestOutput: parsed.enableBestOutput, enableEarlyStopping: parsed.enableEarlyStopping, loopId: parsed.loopId, provider: parsed.provider, dangerous: parsed.dangerous, params: parsed.params, verbose: parsed.verbose, logFile: parsed.logFile, }; const result = await launchExternalRalph(ctx.frameworkRoot, process.cwd(), options); if (parsed.attach) { process.stdout.write(`✓ ${result.message}\n PID: ${result.pid}\n Loop ID: ${result.loopId}\n\n` + `Attaching to output... Press Ctrl+C to detach (loop keeps running).\n\n`); await attachToLoopOutput(process.cwd(), result.loopId); return { exitCode: 0, message: '' }; } return { exitCode: 0, message: `✓ ${result.message}\n PID: ${result.pid}\n Loop ID: ${result.loopId}`, }; } catch (error) { const result = handlerResultFromError(error); return { ...result, message: `Failed to launch Ralph: ${result.message}` }; } } getHelpText() { return ` Agent Loop - Crash-resilient iterative task execution USAGE: aiwg ralph "<objective>" --completion "<criteria>" [options] aiwg ralph-status [--all] aiwg ralph-abort [--loop-id <id>] aiwg ralph-resume [--loop-id <id>] ARGUMENTS: <objective> Task objective (required) OPTIONS: -c, --completion <str> Completion criteria (required) --max-iterations <n> Maximum 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 --loop-id <id> Use specific loop ID --provider <name> Agent system to use (default: claude) Spawnable: claude, opencode, codex, hermes --dangerous Enable unrestricted mode for the selected provider. Passes the provider's native flag (e.g. --dangerously-skip-permissions for claude/opencode, --full-auto for codex). No effect if the provider doesn't have a dangerous mode flag. --params "<args>" Pass arbitrary args verbatim to the agent binary. Appended after all other flags. Quoted segments preserved. --attach Stay attached to the loop's output after launch. Press Ctrl+C to detach (loop keeps running in background). -v, --verbose Enable verbose per-iteration detail in the daemon output: assessment results, planned strategy, and prompt preview. --log-file <path> Write a timestamped copy of all daemon output to <path> (in addition to the per-loop daemon-output.log). Useful for capturing a single readable log across all iterations. Use 'aiwg ralph-attach' to re-attach later. RESEARCH-BACKED OPTIONS (REF-015, REF-021): -m, --memory <n> Memory capacity Ί (default: 3) --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 --no-early-stopping Disable early stopping on high confidence FLAGS: --internal Run in foreground (legacy mode) -h, --help Show this help message EXAMPLES: # Start background loop aiwg ralph "Fix all failing tests" --completion "npm test passes" # Check status aiwg ralph-status # Abort running loop aiwg ralph-abort `; } } /** * Ralph Status Handler * * Shows current Agent loop status and iteration history. * Works across terminal sessions by reading from the loop registry. */ export class RalphStatusHandler { id = 'ralph-status'; name = 'Ralph Status'; description = 'Show Agent loop status and iteration history'; category = 'ralph'; aliases = ['ralph-status']; async execute(ctx) { const showAll = ctx.args.includes('--all') || ctx.args.includes('-a'); const statuses = getLoopStatuses(process.cwd()); if (statuses.length === 0) { return { exitCode: 0, message: 'No Agent loops found.', }; } // Sort by most recent first statuses.sort((a, b) => new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime()); // Filter to running only unless --all const displayed = showAll ? statuses : statuses.filter((s) => s.status === 'running'); if (displayed.length === 0) { return { exitCode: 0, message: 'No running Agent loops. Use --all to see completed/failed loops.', }; } const lines = ['Agent Loop Status', '=================', '']; for (const loop of displayed) { const statusIcon = loop.status === 'running' ? '🔄' : loop.status === 'completed' ? '✅' : loop.status === 'aborted' ? 'âšī¸' : '❌'; lines.push(`${statusIcon} ${loop.loopId}`); lines.push(` Status: ${loop.status.toUpperCase()}`); lines.push(` Progress: ${loop.iteration}/${loop.maxIterations} iterations`); lines.push(` Objective: ${loop.objective.slice(0, 60)}${loop.objective.length > 60 ? '...' : ''}`); lines.push(` Started: ${loop.startedAt}`); if (loop.status === 'running') { lines.push(` PID: ${loop.pid}`); } if (loop.outputFile) { lines.push(` Daemon log: ${loop.outputFile}`); } if (loop.sessionStdoutFile) { lines.push(` Session stdout: ${loop.sessionStdoutFile}`); } if (loop.sessionStderrFile) { lines.push(` Session stderr: ${loop.sessionStderrFile}`); } lines.push(''); } if (!showAll && statuses.length > displayed.length) { lines.push(`(${statuses.length - displayed.length} completed/failed loops hidden. Use --all to show.)`); } return { exitCode: 0, message: lines.join('\n'), }; } } /** * Ralph Abort Handler * * Aborts currently running Agent loop by killing the background process. * Works across terminal sessions. */ export class RalphAbortHandler { id = 'ralph-abort'; name = 'Ralph Abort'; description = 'Abort currently running Agent loop'; category = 'ralph'; aliases = ['ralph-abort']; async execute(ctx) { // Parse --loop-id if provided let loopId; const loopIdIndex = ctx.args.indexOf('--loop-id'); if (loopIdIndex !== -1 && ctx.args[loopIdIndex + 1]) { loopId = ctx.args[loopIdIndex + 1]; } const result = abortLoop(process.cwd(), loopId); return { exitCode: result.success ? 0 : 1, message: result.message, }; } } /** * Ralph Resume Handler * * Resumes previously interrupted Agent loop as a new background process. * Works across terminal sessions. */ export class RalphResumeHandler { id = 'ralph-resume'; name = 'Ralph Resume'; description = 'Resume previously aborted Agent loop'; category = 'ralph'; aliases = ['ralph-resume']; async execute(ctx) { // Parse arguments let loopId; let maxIterations; for (let i = 0; i < ctx.args.length; i++) { const arg = ctx.args[i]; if (arg === '--loop-id' && ctx.args[i + 1]) { loopId = ctx.args[++i]; } else if (arg === '--max-iterations' && ctx.args[i + 1]) { maxIterations = parseInt(ctx.args[++i], 10); } } try { const result = await resumeLoop(ctx.frameworkRoot, process.cwd(), loopId, { maxIterations }); return { exitCode: 0, message: `✓ ${result.message}\n PID: ${result.pid}\n Loop ID: ${result.loopId}`, }; } catch (error) { const result = handlerResultFromError(error); return { ...result, message: `Failed to resume: ${result.message}` }; } } } /** * Create Ralph handler instance */ export function createRalphHandler() { return new RalphHandler(); } /** * Create Ralph Status handler instance */ export function createRalphStatusHandler() { return new RalphStatusHandler(); } /** * Create Ralph Abort handler instance */ export function createRalphAbortHandler() { return new RalphAbortHandler(); } /** * Create Ralph Resume handler instance */ export function createRalphResumeHandler() { return new RalphResumeHandler(); } /** * Ralph External handler * * Crash-resilient external loop with full state persistence. * Equivalent to `ralph` with crash-recovery enabled by default. */ export const ralphExternalHandler = { id: 'agent-loop-ext', name: 'Agent Loop External', description: 'Crash-resilient external loop with state persistence and CI/CD integration', category: 'ralph', aliases: ['ralph-external', '--ralph-external', '--agent-loop-ext'], async execute(ctx) { const runner = createScriptRunner(ctx.frameworkRoot); // Delegate to the external Ralph supervisor script return runner.run('tools/ralph-external/index.mjs', ctx.args); }, }; /** * Ralph Memory handler * * Manage semantic memory entries for Agent loop learning. */ export const ralphMemoryHandler = { id: 'ralph-memory', name: 'Ralph Memory', description: 'Manage Ralph semantic memory entries (list, query, clear)', category: 'ralph', aliases: ['--ralph-memory'], async execute(ctx) { const runner = createScriptRunner(ctx.frameworkRoot); return runner.run('tools/ralph-external/memory-manager.mjs', ['--cli', ...ctx.args]); }, }; /** * Ralph Config handler * * View and set Agent loop configuration values. */ export const ralphConfigHandler = { id: 'ralph-config', name: 'Ralph Config', description: 'View and configure Agent loop settings (show, set, reset, preset)', category: 'ralph', aliases: ['--ralph-config'], async execute(ctx) { const runner = createScriptRunner(ctx.frameworkRoot); return runner.run('tools/ralph-external/orchestrator.mjs', ['--config', ...ctx.args]); }, }; /** * Ralph Attach Handler * * Re-attaches to a running Agent loop's live output stream. * Press Ctrl+C to detach — loop continues running in background. */ export class RalphAttachHandler { id = 'ralph-attach'; name = 'Ralph Attach'; description = 'Attach to a running Agent loop\'s live output stream'; category = 'ralph'; aliases = ['ralph-attach']; async execute(ctx) { let loopId; const loopIdIndex = ctx.args.indexOf('--loop-id'); if (loopIdIndex !== -1 && ctx.args[loopIdIndex + 1]) { loopId = ctx.args[loopIdIndex + 1]; } try { process.stdout.write(`Attaching to loop output${loopId ? ` (${loopId})` : ''}...\n` + `Press Ctrl+C to detach (loop keeps running in background).\n\n`); await attachToLoopOutput(process.cwd(), loopId); return { exitCode: 0, message: '' }; } catch (error) { const result = handlerResultFromError(error); return { ...result, message: `Failed to attach: ${result.message}` }; } } } /** * Export handler instances */ export const ralphHandler = new RalphHandler(); export const ralphStatusHandler = new RalphStatusHandler(); export const ralphAbortHandler = new RalphAbortHandler(); export const ralphResumeHandler = new RalphResumeHandler(); export const ralphAttachHandler = new RalphAttachHandler(); /** * Export all Ralph handlers as array for easy registration */ export const ralphHandlers = [ ralphHandler, ralphStatusHandler, ralphAbortHandler, ralphResumeHandler, ralphAttachHandler, ralphExternalHandler, ralphMemoryHandler, ralphConfigHandler, ]; //# sourceMappingURL=ralph.js.map