Skip to content

Architecture

ralph is designed to be simple, composable, and tool-agnostic. This document explains how it works under the hood.

┌─────────────────────────────────────────────────────────────────┐
│ ralph │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Config │──▶│ Loop │──▶│ Adapter │──▶│ Complete │ │
│ │ Loader │ │ Engine │ │ Runner │ │ Checker │ │
│ └──────────┘ └────┬─────┘ └──────────┘ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ Hooks │ │
│ │ Executor │ │
│ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ AI Tool (External) │
│ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ Claude │ │OpenCode│ │ Gemini │ │
│ │ Code │ │ │ │ (dev) │ │
│ └────────┘ └────────┘ └────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

Loads and validates configuration from multiple sources.

Priority (lowest to highest):
1. Default values
2. ~/.ralph/config.toml (user config)
3. .ralph/config.toml (project config)
4. CLI arguments

The heart of ralph. Manages the iteration lifecycle.

// Simplified loop logic
async function runLoop(config: Config): Promise<Result> {
let iteration = 0;
while (iteration < config.maxIterations) {
iteration++;
// Pre-iteration hooks
await runHooks('ralph_loop_start', { iteration });
// Run the AI tool
const result = await runAdapter(config.adapter, config.prompt);
// Post-iteration hooks
await runHooks('ralph_loop_end', { iteration });
// Check for completion marker
if (result.output.includes('<promise>COMPLETE</promise>')) {
await runHooks('ralph_complete', { iteration });
return { success: true, iterations: iteration };
}
}
await runHooks('ralph_max_iterations', { iteration });
return { success: false, reason: 'maxIterations' };
}

Abstracts the interface to different AI tools.

interface Adapter {
name: string;
supportedFormats: OutputFormat[];
buildArgs(prompt: string): string[];
checkAvailability(): boolean;
}

Each supported tool has an adapter:

  • ClaudeAdapter — for Claude Code
  • OpenCodeAdapter — for OpenCode
  • GeminiAdapter — for Gemini (under development)

Evaluates if the task is complete after each iteration.

function checkCompletion(output: string): boolean {
return output.includes('<promise>COMPLETE</promise>');
}

Runs shell commands at key points in the lifecycle.

type HookPoint =
| 'ralph_start' // Before first iteration
| 'ralph_loop_start' // Before each iteration
| 'ralph_loop_end' // After each iteration
| 'ralph_complete' // When task completes
| 'ralph_max_iterations'; // When limit reached
CLI args → Config Loader → Validated Config
├── Load prompt file
├── Initialize adapter
└── Run ralph_start hook
Loop Engine
├── 1. ralph_loop_start hook
├── 2. Adapter Runner
│ │
│ ├── Start AI process
│ ├── Stream output
│ ├── Track tokens
│ └── Wait for exit
├── 3. ralph_loop_end hook
├── 4. Completion Checker
│ │
│ └── Check for <promise>COMPLETE</promise>
└── 5. Decision: Continue or Exit
Completion marker found
├── Run ralph_complete hook
├── Display summary (tokens, cost, iterations)
└── Exit with code 0
class ClaudeAdapter implements Adapter {
name = 'claude';
supportedFormats = ['stream-json', 'text'];
buildArgs(prompt: string): string[] {
return [
'--permission-mode', 'acceptEdits',
'--prompt', prompt
];
}
}
class OpenCodeAdapter implements Adapter {
name = 'opencode';
supportedFormats = ['opencode-json', 'text'];
buildArgs(prompt: string): string[] {
return ['run', '--prompt', prompt];
}
}
project/
├── .ralph/
│ └── config.toml # Configuration
└── .plans/
├── prd.json # Feature requirements
├── PROMPT.md # System prompt
└── progress.txt # Learning log
interface Config {
adapter: 'claude' | 'opencode' | 'gemini';
plansDir: string;
maxIterations: number;
verbose: boolean;
tui: boolean;
showUsage: boolean;
hooks: {
ralph_start?: string;
ralph_loop_start?: string;
ralph_loop_end?: string;
ralph_complete?: string;
ralph_max_iterations?: string;
};
}