aiwg
Version:
Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.
417 lines (366 loc) âĸ 12.5 kB
text/typescript
/**
* 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 { CommandHandler, HandlerContext, HandlerResult } from './types.js';
import { createScriptRunner } from './script-runner.js';
import {
launchExternalRalph,
getLoopStatuses,
abortLoop,
resumeLoop,
RalphLaunchOptions,
} from './ralph-launcher.js';
/**
* Parse Ralph command arguments
*/
function parseRalphArgs(args: string[]): {
objective?: string;
completionCriteria?: string;
maxIterations?: number;
model?: string;
budget?: number;
timeout?: number;
mcpConfig?: string;
giteaIssue?: boolean;
memory?: number | string;
crossTask?: boolean;
enableAnalytics?: boolean;
enableBestOutput?: boolean;
enableEarlyStopping?: boolean;
internal?: boolean;
help?: boolean;
loopId?: string;
provider?: string;
} {
const result: ReturnType<typeof parseRalphArgs> = {};
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.startsWith('-') && !result.objective) {
result.objective = arg;
}
i++;
}
return result;
}
/**
* Ralph 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 implements CommandHandler {
id = 'ralph';
name = 'Ralph Loop';
description = 'Execute iterative task loop with automatic completion detection';
category = 'ralph' as const;
aliases = ['ralph', '-ralph', '--ralph'];
async execute(ctx: HandlerContext): Promise<HandlerResult> {
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: RalphLaunchOptions = {
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,
};
const result = await launchExternalRalph(ctx.frameworkRoot, process.cwd(), options);
return {
exitCode: 0,
message: `â ${result.message}\n PID: ${result.pid}\n Loop ID: ${result.loopId}`,
};
} catch (error) {
return {
exitCode: 1,
message: `Failed to launch Ralph: ${error instanceof Error ? error.message : String(error)}`,
error: error instanceof Error ? error : new Error(String(error)),
};
}
}
private getHelpText(): string {
return `
Ralph 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> CLI provider: claude (default), codex
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 Ralph loop status and iteration history.
* Works across terminal sessions by reading from the loop registry.
*/
export class RalphStatusHandler implements CommandHandler {
id = 'ralph-status';
name = 'Ralph Status';
description = 'Show Ralph loop status and iteration history';
category = 'ralph' as const;
aliases = ['ralph-status'];
async execute(ctx: HandlerContext): Promise<HandlerResult> {
const showAll = ctx.args.includes('--all') || ctx.args.includes('-a');
const statuses = getLoopStatuses(process.cwd());
if (statuses.length === 0) {
return {
exitCode: 0,
message: 'No Ralph 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 Ralph loops. Use --all to see completed/failed loops.',
};
}
const lines: string[] = ['Ralph 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}`);
}
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 Ralph loop by killing the background process.
* Works across terminal sessions.
*/
export class RalphAbortHandler implements CommandHandler {
id = 'ralph-abort';
name = 'Ralph Abort';
description = 'Abort currently running Ralph loop';
category = 'ralph' as const;
aliases = ['ralph-abort'];
async execute(ctx: HandlerContext): Promise<HandlerResult> {
// Parse --loop-id if provided
let loopId: string | undefined;
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 Ralph loop as a new background process.
* Works across terminal sessions.
*/
export class RalphResumeHandler implements CommandHandler {
id = 'ralph-resume';
name = 'Ralph Resume';
description = 'Resume previously aborted Ralph loop';
category = 'ralph' as const;
aliases = ['ralph-resume'];
async execute(ctx: HandlerContext): Promise<HandlerResult> {
// Parse arguments
let loopId: string | undefined;
let maxIterations: number | undefined;
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) {
return {
exitCode: 1,
message: `Failed to resume: ${error instanceof Error ? error.message : String(error)}`,
error: error instanceof Error ? error : new Error(String(error)),
};
}
}
}
/**
* Create Ralph handler instance
*/
export function createRalphHandler(): CommandHandler {
return new RalphHandler();
}
/**
* Create Ralph Status handler instance
*/
export function createRalphStatusHandler(): CommandHandler {
return new RalphStatusHandler();
}
/**
* Create Ralph Abort handler instance
*/
export function createRalphAbortHandler(): CommandHandler {
return new RalphAbortHandler();
}
/**
* Create Ralph Resume handler instance
*/
export function createRalphResumeHandler(): CommandHandler {
return new RalphResumeHandler();
}
/**
* Export handler instances
*/
export const ralphHandler = new RalphHandler();
export const ralphStatusHandler = new RalphStatusHandler();
export const ralphAbortHandler = new RalphAbortHandler();
export const ralphResumeHandler = new RalphResumeHandler();
/**
* Export all Ralph handlers as array for easy registration
*/
export const ralphHandlers: CommandHandler[] = [
ralphHandler,
ralphStatusHandler,
ralphAbortHandler,
ralphResumeHandler,
];