/** * Ralph Wiggum Loop Runner for OpenCode * A Windows-agnostic TypeScript implementation of the Ralph Loop pattern. * * Usage: bun ralph.ts "task description" [max-iterations] [stack] * * The Ralph Loop: * 1. Orientation: Reads existing plan.md and agent guidelines * 2. Task Selection: Picks the first unchecked [ ] task * 3. Implementation: Makes the smallest possible change * 4. Verification: Runs tests and type checks * 5. Commit: Creates a git commit with proper format * 6. Update Plan: Marks task complete, adds new discoveries * 7. Repeat: Until all tasks are done or max iterations reached */ import { $ } from "bun" import { existsSync, readFileSync } from "fs" import { join } from "path" // Configuration const COMPLETION_SIGNAL = "RALPH_COMPLETE" const BLOCKED_SIGNAL = "[BLOCKED]" const DELAY_MS = 2000 // Colors for console output const colors = { reset: "\x1b[0m", bright: "\x1b[1m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m", blue: "\x1b[34m", cyan: "\x1b[36m", } function log(message: string, color?: keyof typeof colors) { const prefix = color ? colors[color] : "" const suffix = color ? colors.reset : "" console.log(`${prefix}${message}${suffix}`) } function logHeader(iteration: number, maxIterations: number) { console.log("") log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "cyan") log(` Iteration ${iteration} / ${maxIterations}`, "bright") log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "cyan") console.log("") } function getPlanContent(): string { const planPath = join(process.cwd(), "plan.md") if (!existsSync(planPath)) { return "No plan.md found. Create one with tasks as checkboxes: - [ ] Task description" } return readFileSync(planPath, "utf-8") } function getAgentGuidelines(): string { const agentsPath = join(process.cwd(), "AGENTS.md") if (!existsSync(agentsPath)) { return "" } return readFileSync(agentsPath, "utf-8") } function buildRalphPrompt(task: string, stack: string): string { const plan = getPlanContent() const guidelines = getAgentGuidelines() return `You are Ralph, an autonomous coding agent following the Ralph Loop pattern. ## Your Task ${task} ## Stack ${stack} ## Current Plan (plan.md) \`\`\`markdown ${plan} \`\`\` ${guidelines ? `## Agent Guidelines (AGENTS.md)\n\`\`\`markdown\n${guidelines}\n\`\`\`\n` : ""} ## Ralph Loop Instructions 1. **Orientation**: Review the plan.md and guidelines above 2. **Task Selection**: Find the first unchecked [ ] task in the plan 3. **Implementation**: Make the SMALLEST possible change to complete that task 4. **Verification**: Run appropriate tests/linting (bun lint, bun run bb) 5. **Commit**: Create a git commit with a descriptive message 6. **Update Plan**: - Mark the completed task as [x] in plan.md - Add any new discoveries or subtasks you found 7. **Completion Check**: - If ALL tasks are now [x] complete, output: ${COMPLETION_SIGNAL} - If you've tried a task 3+ times and can't complete it, prefix it with ${BLOCKED_SIGNAL} and move on ## Important Rules - Make ONE small change per iteration - Always verify your changes work before committing - Be honest about blocked tasks - mark them and move on - Output ${COMPLETION_SIGNAL} ONLY when ALL tasks are genuinely complete Now execute one iteration of the Ralph Loop.` } async function runOpencode(prompt: string): Promise<{ output: string; exitCode: number }> { try { // Escape the prompt for shell const result = await $`opencode run ${prompt}`.text() return { output: result, exitCode: 0 } } catch (error) { const err = error as { stdout?: Buffer; stderr?: Buffer; exitCode?: number } const output = [err.stdout?.toString() ?? "", err.stderr?.toString() ?? ""] .filter(Boolean) .join("\n") return { output: output || String(error), exitCode: err.exitCode ?? 1 } } } async function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } async function main() { const args = process.argv.slice(2) // Parse arguments const task = args[0] const maxIterations = parseInt(args[1] ?? "25", 10) const stack = args[2] ?? "full" if (!task) { log('Usage: bun ralph.ts "task description" [max-iterations] [stack]', "yellow") log("", "reset") log("Arguments:", "bright") log(" task - Description of what Ralph should accomplish (required)") log(" max-iterations - Maximum loop iterations (default: 25)") log(" stack - Technology stack context (default: full)") log("", "reset") log("Example:", "bright") log(' bun ralph.ts "Implement user authentication" 30 nextjs') process.exit(1) } // Check if plan.md exists, create template if not const planPath = join(process.cwd(), "plan.md") if (!existsSync(planPath)) { log("Warning: No plan.md found in current directory.", "yellow") log("Ralph will ask opencode to create initial tasks.", "dim") console.log("") } // Print startup banner console.log("") log("╔═══════════════════════════════════════════════════════════╗", "cyan") log("║ Ralph Wiggum Loop Runner for OpenCode ║", "cyan") log("╚═══════════════════════════════════════════════════════════╝", "cyan") console.log("") log(` Task: ${task}`, "bright") log(` Max Iterations: ${maxIterations}`, "dim") log(` Stack: ${stack}`, "dim") console.log("") let iteration = 0 let blockedCount = 0 while (iteration < maxIterations) { iteration++ logHeader(iteration, maxIterations) // Build the prompt with current plan state const prompt = buildRalphPrompt(task, stack) // Run opencode log("Running opencode...", "dim") const { output, exitCode } = await runOpencode(prompt) // Display output console.log("") console.log(output) console.log("") // Check for completion if (output.includes(COMPLETION_SIGNAL)) { console.log("") log("╔═══════════════════════════════════════════════════════════╗", "green") log( `║ Ralph completed successfully after ${iteration} iteration(s)!`.padEnd(60) + "║", "green" ) log("╚═══════════════════════════════════════════════════════════╝", "green") console.log("") process.exit(0) } // Check for blocked tasks if (output.includes(BLOCKED_SIGNAL)) { blockedCount++ log( `Warning: Blocked task detected (${blockedCount} total). Check plan.md for details.`, "yellow" ) } // Log exit code if non-zero if (exitCode !== 0) { log(`opencode exited with code ${exitCode}`, "yellow") } // Delay before next iteration to prevent rate limiting if (iteration < maxIterations) { log(`Waiting ${DELAY_MS / 1000}s before next iteration...`, "dim") await sleep(DELAY_MS) } } // Max iterations reached console.log("") log("╔═══════════════════════════════════════════════════════════╗", "yellow") log(`║ Max iterations (${maxIterations}) reached.`.padEnd(60) + "║", "yellow") log("║ Check plan.md for remaining tasks.".padEnd(60) + "║", "yellow") log("╚═══════════════════════════════════════════════════════════╝", "yellow") console.log("") if (blockedCount > 0) { log(`Total blocked tasks encountered: ${blockedCount}`, "red") } process.exit(1) } // Run main main().catch((error) => { log(`Fatal error: ${error}`, "red") process.exit(1) })