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
JavaScript
/**
* 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