Initial commit: Ralph Wiggum plugin for OpenCode

This commit is contained in:
rot13maxi 2026-01-07 00:08:27 +00:00
commit d8f58ffc3c
6 changed files with 677 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# Ralph loop state file (should not be committed)
ralph-loop.local.md

225
README.md Normal file
View file

@ -0,0 +1,225 @@
# Ralph Wiggum Plugin for OpenCode
Implementation of the Ralph Wiggum technique for iterative, self-referential AI development loops in OpenCode.
Ported from the [Claude Code Ralph Plugin](https://github.com/anthropics/claude-code/tree/main/plugins/ralph-wiggum).
## What is Ralph?
Ralph is a development methodology based on continuous AI agent loops. As Geoffrey Huntley describes it: **"Ralph is a Bash loop"** - a simple `while true` that repeatedly feeds an AI agent a prompt file, allowing it to iteratively improve its work until completion.
The technique is named after Ralph Wiggum from The Simpsons, embodying the philosophy of persistent iteration despite setbacks.
### Core Concept
This plugin implements Ralph using OpenCode's event system to intercept session idle states:
```bash
# You run ONCE:
/ralph-loop "Your task description" --completion-promise "DONE"
# Then OpenCode automatically:
# 1. Works on the task
# 2. Finishes responding
# 3. Plugin intercepts idle state
# 4. Plugin feeds the SAME prompt back
# 5. Repeat until completion
```
The loop happens **inside your current session** - you don't need external bash loops. The plugin creates the self-referential feedback loop by intercepting the session idle event.
This creates a **self-referential feedback loop** where:
- The prompt never changes between iterations
- The AI's previous work persists in files
- Each iteration sees modified files and git history
- The AI autonomously improves by reading its own past work in files
## Installation
Clone or copy this repo, then symlink or copy to your global OpenCode config:
```bash
# Clone the repo
git clone https://github.com/anthropics/opencode-ralph.git
cd opencode-ralph
# Symlink to global config (recommended - updates automatically)
ln -s "$(pwd)/plugin/ralph.ts" ~/.config/opencode/plugin/ralph.ts
ln -s "$(pwd)/command/ralph-loop.md" ~/.config/opencode/command/ralph-loop.md
ln -s "$(pwd)/command/cancel-ralph.md" ~/.config/opencode/command/cancel-ralph.md
ln -s "$(pwd)/command/ralph-help.md" ~/.config/opencode/command/ralph-help.md
# Or copy files directly
cp plugin/* ~/.config/opencode/plugin/
cp command/* ~/.config/opencode/command/
```
For project-level installation, copy to `.opencode/`:
```bash
cp plugin/* /path/to/your/project/.opencode/plugin/
cp command/* /path/to/your/project/.opencode/command/
```
## Quick Start
```bash
/ralph-loop "Build a REST API for todos. Requirements: CRUD operations, input validation, tests. Output <promise>COMPLETE</promise> when done." --completion-promise "COMPLETE" --max-iterations 50
```
The AI will:
- Implement the API iteratively
- Run tests and see failures
- Fix bugs based on test output
- Iterate until all requirements met
- Output the completion promise when done
## Commands
### /ralph-loop
Start a Ralph loop in your current session.
**Usage:**
```bash
/ralph-loop "<prompt>" --max-iterations <n> --completion-promise "<text>"
```
**Options:**
- `--max-iterations <n>` - Stop after N iterations (default: unlimited)
- `--completion-promise <text>` - Phrase that signals completion
### /cancel-ralph
Cancel the active Ralph loop.
**Usage:**
```bash
/cancel-ralph
```
### /ralph-help
Get detailed help about the Ralph technique and commands.
**Usage:**
```bash
/ralph-help
```
## Prompt Writing Best Practices
### 1. Clear Completion Criteria
Bad: "Build a todo API and make it good."
Good:
```markdown
Build a REST API for todos.
When complete:
- All CRUD endpoints working
- Input validation in place
- Tests passing (coverage > 80%)
- README with API docs
- Output: <promise>COMPLETE</promise>
```
### 2. Incremental Goals
Bad: "Create a complete e-commerce platform."
Good:
```markdown
Phase 1: User authentication (JWT, tests)
Phase 2: Product catalog (list/search, tests)
Phase 3: Shopping cart (add/remove, tests)
Output <promise>COMPLETE</promise> when all phases done.
```
### 3. Self-Correction
Bad: "Write code for feature X."
Good:
```markdown
Implement feature X following TDD:
1. Write failing tests
2. Implement feature
3. Run tests
4. If any fail, debug and fix
5. Refactor if needed
6. Repeat until all green
7. Output: <promise>COMPLETE</promise>
```
### 4. Escape Hatches
Always use `--max-iterations` as a safety net to prevent infinite loops:
```bash
# Recommended: Always set a reasonable iteration limit
/ralph-loop "Try to implement feature X" --max-iterations 20
```
## Philosophy
Ralph embodies several key principles:
### 1. Iteration > Perfection
Don't aim for perfect on first try. Let the loop refine the work.
### 2. Failures Are Data
"Deterministically bad" means failures are predictable and informative. Use them to tune prompts.
### 3. Operator Skill Matters
Success depends on writing good prompts, not just having a good model.
### 4. Persistence Wins
Keep trying until success. The loop handles retry logic automatically.
## When to Use Ralph
**Good for:**
- Well-defined tasks with clear success criteria
- Tasks requiring iteration and refinement (e.g., getting tests to pass)
- Greenfield projects where you can walk away
- Tasks with automatic verification (tests, linters)
**Not good for:**
- Tasks requiring human judgment or design decisions
- One-shot operations
- Tasks with unclear success criteria
- Production debugging (use targeted debugging instead)
## Files
- `plugin/ralph.ts` - Main plugin that handles the loop logic
- `command/ralph-loop.md` - Command to start a Ralph loop
- `command/cancel-ralph.md` - Command to cancel the loop
- `command/ralph-help.md` - Help documentation
## State File
The plugin stores loop state in `ralph-loop.local.md` in your project root:
```markdown
---
active: true
iteration: 5
max_iterations: 20
completion_promise: "DONE"
started_at: "2024-01-15T10:30:00Z"
---
Your prompt text here...
```
Add this file to `.gitignore` to avoid committing loop state.
## Learn More
- Original technique: https://ghuntley.com/ralph/
- Ralph Orchestrator: https://github.com/mikeyobrien/ralph-orchestrator
- Claude Code plugin: https://github.com/anthropics/claude-code/tree/main/plugins/ralph-wiggum

28
command/cancel-ralph.md Normal file
View file

@ -0,0 +1,28 @@
---
description: Cancel the active Ralph Wiggum loop
---
# Cancel Ralph Loop
To cancel the Ralph loop, perform these steps:
1. Check if the Ralph state file exists at `ralph-loop.local.md`
2. If the file does NOT exist:
- Report: "No active Ralph loop found."
3. If the file EXISTS:
- Read the file to get the current iteration number from the `iteration:` field in the frontmatter
- Delete the file `ralph-loop.local.md`
- Report: "Cancelled Ralph loop (was at iteration N)" where N is the iteration value
Execute:
```bash
if [ -f ralph-loop.local.md ]; then
ITERATION=$(grep '^iteration:' ralph-loop.local.md | sed 's/iteration: *//')
rm ralph-loop.local.md
echo "Cancelled Ralph loop (was at iteration $ITERATION)"
else
echo "No active Ralph loop found."
fi
```

126
command/ralph-help.md Normal file
View file

@ -0,0 +1,126 @@
---
description: Explain the Ralph Wiggum technique and available commands
---
# Ralph Wiggum Plugin Help
Please explain the following to the user:
## What is the Ralph Wiggum Technique?
The Ralph Wiggum technique is an iterative development methodology based on continuous AI loops, pioneered by Geoffrey Huntley.
**Core concept:**
```bash
while :; do
cat PROMPT.md | opencode --continue
done
```
The same prompt is fed to the AI repeatedly. The "self-referential" aspect comes from the AI seeing its own previous work in the files and git history, not from feeding output back as input.
**Each iteration:**
1. AI receives the SAME prompt
2. Works on the task, modifying files
3. Completes its response
4. Plugin intercepts idle state and feeds the same prompt again
5. AI sees its previous work in the files
6. Iteratively improves until completion
The technique is described as "deterministically bad in an undeterministic world" - failures are predictable, enabling systematic improvement through prompt tuning.
## Available Commands
### /ralph-loop <PROMPT> [OPTIONS]
Start a Ralph loop in your current session.
**Usage:**
```
/ralph-loop "Refactor the cache layer" --max-iterations 20
/ralph-loop "Add tests" --completion-promise "TESTS COMPLETE"
```
**Options:**
- `--max-iterations <n>` - Max iterations before auto-stop
- `--completion-promise <text>` - Promise phrase to signal completion
**How it works:**
1. Creates `.opencode/ralph-loop.local.md` state file
2. You work on the task
3. When you finish responding, the plugin intercepts
4. Same prompt fed back
5. You see your previous work
6. Continues until promise detected or max iterations
---
### /cancel-ralph
Cancel an active Ralph loop (removes the loop state file).
**Usage:**
```
/cancel-ralph
```
**How it works:**
- Checks for active loop state file
- Removes `.opencode/ralph-loop.local.md`
- Reports cancellation with iteration count
---
## Key Concepts
### Completion Promises
To signal completion, the AI must output a `<promise>` tag:
```
<promise>TASK COMPLETE</promise>
```
The plugin looks for this specific tag. Without it (or `--max-iterations`), Ralph runs infinitely.
### Self-Reference Mechanism
The "loop" doesn't mean the AI talks to itself. It means:
- Same prompt repeated
- AI's work persists in files
- Each iteration sees previous attempts
- Builds incrementally toward goal
## Example
### Interactive Bug Fix
```
/ralph-loop "Fix the token refresh logic in auth.ts. Output <promise>FIXED</promise> when all tests pass." --completion-promise "FIXED" --max-iterations 10
```
You'll see Ralph:
- Attempt fixes
- Run tests
- See failures
- Iterate on solution
- In your current session
## When to Use Ralph
**Good for:**
- Well-defined tasks with clear success criteria
- Tasks requiring iteration and refinement
- Iterative development with self-correction
- Greenfield projects
**Not good for:**
- Tasks requiring human judgment or design decisions
- One-shot operations
- Tasks with unclear success criteria
- Debugging production issues (use targeted debugging instead)
## Learn More
- Original technique: https://ghuntley.com/ralph/
- Ralph Orchestrator: https://github.com/mikeyobrien/ralph-orchestrator

80
command/ralph-loop.md Normal file
View file

@ -0,0 +1,80 @@
---
description: Start a Ralph Wiggum loop for iterative development
---
# Ralph Loop Command
You are starting a Ralph Wiggum loop. This is an iterative development technique where you work on the same task repeatedly, seeing your previous work in files and git history.
## Setup Instructions
Execute the following steps to initialize the Ralph loop:
1. Parse the arguments from: `$ARGUMENTS`
Arguments format: `<PROMPT> [--max-iterations N] [--completion-promise TEXT]`
- Extract the main prompt (everything that isn't a flag or flag value)
- Extract `--max-iterations` value if provided (default: 0 for unlimited)
- Extract `--completion-promise` value if provided (default: null)
2. Create the state file at `ralph-loop.local.md` (in the project root) with this exact format:
```markdown
---
active: true
iteration: 1
max_iterations: <MAX_ITERATIONS_VALUE>
completion_promise: <COMPLETION_PROMISE_VALUE_OR_null>
started_at: "<CURRENT_ISO_TIMESTAMP>"
---
<THE_PROMPT_TEXT>
```
3. Output the activation message:
```
Ralph loop activated!
Iteration: 1
Max iterations: <N or "unlimited">
Completion promise: <TEXT or "none (runs forever)">
The Ralph plugin will now monitor for session idle events. When you complete
your response, the same prompt will be fed back to continue the loop.
To stop the loop:
- Output <promise>YOUR_PROMISE</promise> if a completion promise is set
- Wait for max iterations to be reached
- Run /cancel-ralph to cancel manually
```
4. If a completion promise is set, display this critical warning:
```
CRITICAL - Ralph Loop Completion Promise
To complete this loop, output this EXACT text:
<promise>YOUR_PROMISE_HERE</promise>
STRICT REQUIREMENTS:
- Use <promise> XML tags EXACTLY as shown above
- The statement MUST be completely and unequivocally TRUE
- Do NOT output false statements to exit the loop
- Do NOT lie even if you think you should exit
IMPORTANT: Even if you believe you're stuck or the task is impossible,
you MUST NOT output a false promise. The loop continues until the
promise is GENUINELY TRUE.
```
5. Now begin working on the task from the prompt. The Ralph plugin will automatically continue feeding you the same prompt when you complete your response.
## Example Usage
```
/ralph-loop Build a REST API for todos --completion-promise "DONE" --max-iterations 20
/ralph-loop Fix the auth bug --max-iterations 10
/ralph-loop Refactor the cache layer
```

216
plugin/ralph.ts Normal file
View file

@ -0,0 +1,216 @@
import type { Plugin } from "@opencode-ai/plugin"
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "fs"
import { join } from "path"
/**
* Ralph Wiggum Plugin for OpenCode
*
* Implementation of the Ralph Wiggum technique - continuous self-referential AI loops
* for iterative development. Named after Ralph Wiggum from The Simpsons, embodying
* the philosophy of persistent iteration despite setbacks.
*
* Core concept: Feed the same prompt repeatedly, letting the AI see its previous work
* in files and git history, creating a self-referential feedback loop.
*
* Based on: https://ghuntley.com/ralph/
*/
interface RalphState {
active: boolean
iteration: number
maxIterations: number
completionPromise: string | null
startedAt: string
prompt: string
}
const STATE_FILE = "ralph-loop.local.md"
function parseRalphState(directory: string): RalphState | null {
const statePath = join(directory, STATE_FILE)
if (!existsSync(statePath)) {
return null
}
try {
const content = readFileSync(statePath, "utf-8")
// Parse YAML frontmatter
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
if (!frontmatterMatch) {
return null
}
const [, frontmatter, prompt] = frontmatterMatch
// Parse frontmatter values
const getValue = (key: string): string | null => {
const match = frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, "m"))
if (!match) return null
// Remove surrounding quotes if present
return match[1].replace(/^["'](.*)["']$/, "$1")
}
const active = getValue("active") === "true"
const iteration = parseInt(getValue("iteration") || "1", 10)
const maxIterations = parseInt(getValue("max_iterations") || "0", 10)
const completionPromise = getValue("completion_promise")
const startedAt = getValue("started_at") || new Date().toISOString()
return {
active,
iteration,
maxIterations,
completionPromise: completionPromise === "null" ? null : completionPromise,
startedAt,
prompt: prompt.trim(),
}
} catch {
return null
}
}
function writeRalphState(directory: string, state: RalphState): void {
const statePath = join(directory, STATE_FILE)
const completionPromiseYaml =
state.completionPromise === null ? "null" : `"${state.completionPromise}"`
const content = `---
active: ${state.active}
iteration: ${state.iteration}
max_iterations: ${state.maxIterations}
completion_promise: ${completionPromiseYaml}
started_at: "${state.startedAt}"
---
${state.prompt}
`
writeFileSync(statePath, content, "utf-8")
}
function deleteRalphState(directory: string): boolean {
const statePath = join(directory, STATE_FILE)
if (existsSync(statePath)) {
unlinkSync(statePath)
return true
}
return false
}
function checkCompletionPromise(text: string, promise: string): boolean {
// Extract text from <promise> tags
const promiseMatch = text.match(/<promise>([\s\S]*?)<\/promise>/)
if (!promiseMatch) return false
// Normalize whitespace and compare
const promiseText = promiseMatch[1].trim().replace(/\s+/g, " ")
return promiseText === promise
}
export const RalphPlugin: Plugin = async ({ directory, client, $ }) => {
return {
/**
* Handle session idle event - this is when the AI has finished responding
* and would normally wait for user input. In Ralph mode, we intercept this
* to continue the loop.
*/
event: async ({ event }) => {
if (event.type !== "session.idle") return
const state = parseRalphState(directory)
if (!state || !state.active) return
// Get the last assistant message to check for completion
// We need to check if the completion promise was output
if (state.completionPromise) {
try {
// Use the SDK to get session messages
const session = await client.session.get({ id: event.properties.sessionID })
if (session.messages && session.messages.length > 0) {
// Find the last assistant message
const lastAssistantMsg = [...session.messages]
.reverse()
.find((m) => m.role === "assistant")
if (lastAssistantMsg) {
// Extract text content from message parts
const textContent = lastAssistantMsg.parts
?.filter((p: any) => p.type === "text")
.map((p: any) => p.text)
.join("\n")
if (textContent && checkCompletionPromise(textContent, state.completionPromise)) {
// Completion promise detected - stop the loop
deleteRalphState(directory)
await client.app.log({
service: "ralph-plugin",
level: "info",
message: `Ralph loop completed: detected <promise>${state.completionPromise}</promise>`,
})
return
}
}
}
} catch (err) {
// If we can't check messages, continue the loop
await client.app.log({
service: "ralph-plugin",
level: "warn",
message: `Could not check for completion promise: ${err}`,
})
}
}
// Check max iterations
if (state.maxIterations > 0 && state.iteration >= state.maxIterations) {
deleteRalphState(directory)
await client.app.log({
service: "ralph-plugin",
level: "info",
message: `Ralph loop stopped: max iterations (${state.maxIterations}) reached`,
})
return
}
// Continue the loop - increment iteration and feed prompt back
const nextIteration = state.iteration + 1
writeRalphState(directory, {
...state,
iteration: nextIteration,
})
// Build the continuation message
let systemMsg = `Ralph iteration ${nextIteration}`
if (state.completionPromise) {
systemMsg += ` | To stop: output <promise>${state.completionPromise}</promise> (ONLY when TRUE)`
} else if (state.maxIterations > 0) {
systemMsg += ` / ${state.maxIterations}`
} else {
systemMsg += ` | No completion promise set - loop runs until cancelled`
}
// Log the iteration
await client.app.log({
service: "ralph-plugin",
level: "info",
message: systemMsg,
})
// Append the prompt back to continue the session
// The prompt includes a marker showing the iteration
const continuationPrompt = `[${systemMsg}]\n\n${state.prompt}`
// Use session.send to continue the conversation
await client.session.send({
id: event.properties.sessionID,
text: continuationPrompt,
})
},
}
}