A tiny CLI that drives a Cursor SDK agent in a forever loop toward a goal you describe in GOAL.md. Each iteration is split into a plan phase and an execute phase, the agent maintains its own memory in STATE.md, and the whole thing runs under pm2 so it survives reboots and disconnects.
"Ralph the Wiggum loop" — a small autonomous loop that wakes up, reads its notes, takes one concrete step toward the goal, writes new notes, and stops. Repeat forever.
git clone https://github.com/unarbos/ralph.git
cd ralph
npm install
npm link # puts `ralph` on $PATH
npm install -g pm2 # required for `ralph run/stop/restart/logs`You'll also need:
- A Cursor API key. Either
export CURSOR_API_KEY=...or have it accessible viadoppler secrets get CURSOR_API_KEY --plain(ralph automatically falls back to doppler when the env var is missing). - Node 20+ (the SDK requires it).
ralph init mine97 # scaffold ./mine97/ + register in ~/.ralph/registry.json
vim mine97/GOAL.md # write your mission
ralph run mine97 # start under pm2
ralph logs mine97 # follow the agent's stdout
ralph stop mine97 # graceful stop (drains the in-flight run, ~30s)That's the whole user-facing flow.
+-----------------------------------+
| PLAN PHASE (read-only) |
| - reads GOAL.md, STATE.md |
| - inspects the world (no writes) |
| - writes PLAN.md only |
+-----------------------------------+
|
v
+-----------------------------------+
| EXECUTE PHASE (full tool access) |
| - reads PLAN.md |
| - runs the steps in PLAN.md |
| - rewrites STATE.md |
+-----------------------------------+
|
v
sleep, then loop
Both phases share one Cursor SDK session. Periodically (every N iterations or when a token budget is exceeded) the session is reset and a fresh agent is spawned, which re-bootstraps from STATE.md alone — that's why the operating manual is strict about STATE.md hygiene.
<name>/ control plane for this goal
GOAL.md you author this; agent re-reads every iteration
STATE.md agent's living memory (auto-created, agent rewrites)
PLAN.md plan for the current iteration (auto-created)
ralph.config.cjs pm2 ecosystem (auto-generated)
.gitignore ignores workspace/ and .ralph/
.ralph/ ralph bookkeeping (loop driver only)
state.json iteration cursor + agent id + token totals
iterations/NNNNNN.json full per-iteration transcript (both phases)
plans/NNNNNN.md snapshot of every prior PLAN.md
pm2.out.log, pm2.err.log pm2-captured stdout/stderr
heartbeat touched every loop tick
lock pidfile (prevents double-runs)
workspace/ agent's cwd; has its own .git
.git/
.gitignore Python/ML defaults
...everything the agent creates...
The split between control plane and workspace/ is deliberate: the agent's actual work — code, configs, model weights, training logs, commits — all lives in workspace/ with its own git history, while the control plane stays clean and focused on goal/state/plan.
ralph init <name> create ./<name>/ and register it
ralph run <name|path> start the project under pm2 (idempotent)
ralph stop <name|path> graceful stop (drains current run, ~30s)
ralph restart <name|path> stop + start
ralph logs <name|path> tail pm2 logs (Ctrl-C to detach)
ralph status [<name>] pretty table of running ralph projects
ralph list list every registered project
ralph delete <name> [--rm] pm2 delete + unregister; --rm also removes the dir
ralph loop <GOAL.md> foreground loop (what pm2 invokes; not for daily use)
ralph print-prompt dump the built-in agent operating manual
ralph help this help
<name|path> accepts either a registered project name (looked up in ~/.ralph/registry.json) or an absolute/relative path.
--prompt <path> override the built-in operating manual
--workspace <path> agent's cwd (default: <goal-dir>/workspace)
--state <path> STATE.md path (default: STATE.md next to GOAL.md)
--plan <path> PLAN.md path (default: PLAN.md next to GOAL.md)
--no-plan skip the plan phase; one execute send per iteration
--model <spec> "id" or "id,key=value,..."
default: claude-opus-4-7
example: claude-opus-4-7,thinking=high
--reset-every <n> reset agent session every N iterations (default: 20)
--token-budget <n> reset when usage since last reset exceeds N (default: 800k)
--idle-ms <n> sleep between iterations (default: 2000)
--max-iters <n> stop after N iterations (default: infinity)
--print-prompt print the operating manual and exit
ralph ships with a goal-agnostic operating manual baked into the binary. It tells the agent how the loop works, the difference between control plane and workspace, the two-phase structure, the schema for STATE.md, anti-patterns, and end-of-phase checklists.
See it with:
ralph print-prompt | lessTo customize:
ralph print-prompt > ~/my-prompt.md
$EDITOR ~/my-prompt.md
ralph run mine97 # but you'll need to edit mine97/ralph.config.cjs to add
# args: ["loop", "GOAL.md", "--prompt", "/abs/path/to/my-prompt.md"],+------+ +-----------------+ +-------------------+
| You | -----> | ralph init/run | -----> | pm2 (ralph-name) |
+------+ +-----------------+ +-------------------+
|
spawns/keeps alive
v
+-------------------+
| ralph loop |
| - plan phase |
| - execute phase |
| - reset policy |
| - state recovery |
+-------------------+
|
v
+-------------------+
| Cursor SDK Agent |
| (claude-opus-4-7) |
+-------------------+
|
shell, edit, web,
subagents, etc.
v
+-------------------+
| <name>/workspace/ |
| (the actual work) |
+-------------------+
- Crash recovery: ralph persists
agentId, iteration, token totals to.ralph/state.jsonafter every iteration. On restart it resumes the same Cursor agent session (or creates a fresh one if resume fails) and STATE.md picks up where it left off. - Pid lockfile:
.ralph/lockprevents two ralph processes from racing on the same project. Stale locks are detected viakill -0and cleaned up. - Graceful shutdown:
SIGINT/SIGTERMcancels the in-flight Cursor run (run.cancel()), disposes the agent, releases the lock, exits 0. pm2'skill_timeout: 30000gives this 30s. - SIGUSR1 = force reset: send
SIGUSR1(ortouch .ralph/reset.signal) to dispose the agent and start a fresh session next iteration. - Retry policy: only retries
CursorAgentError.isRetryable(RateLimit/Network) with exponential backoff (1s, 2s, 4s, 8s, max 60s, 6 attempts). Auth/Configuration errors exit non-zero and pm2'sexp_backoff_restart_delaykeeps the restart loop sane. - Iteration log rotation: keeps the last 200 in
.ralph/iterations/, gzips older intoiterations/archive/. - Token-budget reset: when cumulative session usage exceeds
--token-budget(default 800k), the agent is disposed and rebuilt from STATE.md alone. Prevents context bloat over very long runs. - Periodic reset: every
--reset-everyiterations (default 20) the agent is reset on principle.
ralph status # all ralph processes + their iteration cursors
ralph logs mine97 # streaming log tail
ralph logs mine97 --lines 1000 --nostream # backscroll N lines
pm2 monit # live cpu/mem dashboard for everything pm2 runs
pm2 save && pm2 startup systemd # boot persistence (do once after first run)
cat <name>/STATE.md # what the agent has learned
cat <name>/PLAN.md # what it's executing right now
ls <name>/.ralph/iterations/ # full forensic transcripts
ls <name>/.ralph/plans/ # every prior plan
# force the agent to reset its session before next iteration
touch <name>/.ralph/reset.signal
# clean slate (keeps GOAL.md, removes everything else)
ralph stop <name>
rm -rf <name>/{STATE.md,PLAN.md,.ralph}
ralph run <name>ralph looks for the key in this order:
process.env.CURSOR_API_KEYdoppler secrets get CURSOR_API_KEY --plain(whatever doppler resolves for the project's cwd)
If both fail, ralph prints the exact doppler stderr (e.g. "you must provide a token", "You must specify a project") plus the three concrete fixes — so the failure is self-diagnosing.
git clone https://github.com/unarbos/ralph.git
cd ralph
npm install
./node_modules/.bin/tsc --noEmit # type check
./bin/ralph --help # run without npm linkSingle-file implementation at ralph.ts. The bash shim at bin/ralph execs tsx ralph.ts. The built-in operating manual is the BUILTIN_PROMPT constant near the top of the file.
MIT.