# Ralphify — Full Documentation

> The CLI runtime for the ralph format — a skill-like spec for autonomous agent loops. A ralph is a directory with a RALPH.md file. Ralphify runs it.

Source: https://github.com/computerlovetech/ralphify
PyPI: https://pypi.org/project/ralphify/
Docs: https://ralphify.co/docs/

---

A ralph is a directory that defines an autonomous agent loop. Ralphify runs it.

A **ralph** is a directory with a `RALPH.md` file — a skill-like format that bundles a prompt, the commands to run between iterations, and any files the agent needs. **Ralphify** is the CLI runtime that executes them.

```
grow-coverage/
├── RALPH.md               # the loop definition (required)
├── check-coverage.sh      # command that runs each iteration
└── testing-conventions.md # context for the agent
```

```markdown
---
agent: claude -p --dangerously-skip-permissions
commands:
  - name: coverage
    run: ./check-coverage.sh
---

You are an autonomous coding agent working in a loop.
Each iteration, write tests for one untested module, then stop.

Follow the conventions in testing-conventions.md.

## Current coverage

{{ commands.coverage }}
```

```bash
ralph run grow-coverage     # loops until Ctrl+C
```

One directory. One command. Each iteration starts with fresh context and current data — ralphify runs the commands, fills in `{{ placeholders }}`, pipes the prompt to your agent, and loops.

Works with any agent CLI. Swap `claude -p` for Codex, Aider, or your own — just change the `agent` field.

## Install

```bash
uv tool install ralphify    # recommended
pipx install ralphify       # or pipx
pip install ralphify        # or pip
```

## Scaffold a ralph and run it

```bash
ralph scaffold my-ralph
```

This creates a directory with a `RALPH.md` template. Edit it, then run:

```bash
ralph run my-ralph         # loop until Ctrl+C
ralph run my-ralph -n 3    # run 3 iterations
```

Edit `RALPH.md` while the loop is running — changes take effect on the next iteration.

## Or install one with agr

Ralphs are just directories, so you can share them via any git repo. Install a pre-built ralph from GitHub with [agr](https://github.com/computerlovetech/agr):

```bash
agr add owner/repo/my-ralph     # install a ralph from GitHub
ralph run my-ralph              # run it by name
```

agr installs ralphs to `.agents/ralphs/` so they're automatically discovered by `ralph run`.

---

## Why a format

Everyone writing ralph loops ends up with the same scaffolding: a markdown prompt, a few shell commands that surface state between iterations, a while-loop that ties them together. Turning that into a format makes ralphs **shareable**, **versionable**, and **installable** — the same way skills made inner-loop workflows shareable.

Ralphs are to the outer loop what [skills](https://agentskills.io/) are to the inner loop. A skill guides an agent inside a session. A ralph defines what runs *between* sessions.

- **Fresh context, no decay** — Each iteration starts with a clean context window. No conversation bloat, no hallucinated memories, no degradation over time.
- **Commands as feedback** — Commands run each iteration and their output feeds into the prompt. When tests fail, the agent sees the failure output and fixes it — a self-healing feedback loop.
- **Steer while it runs** — The prompt is re-read every iteration. Edit `RALPH.md` while the loop runs and the agent follows your new rules on the next cycle.
- **Progress lives in git** — Every iteration commits to git. If something goes wrong, `git log` shows exactly what happened and `git reset` rolls it back.

---

## Requirements

- Python 3.11+
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) (or [any agent CLI](agents.md) that accepts piped input)

---

## Next steps

- **[The Ralph Format](blog/posts/the-ralph-format.md)** — the full spec
- **[Getting Started](getting-started.md)** — from install to a running loop in 10 minutes
- **[How it Works](how-it-works.md)** — what happens inside each iteration
- **[Cookbook](cookbook.md)** — copy-pasteable ralphs for coding, docs, research, and more
- **[Python API](api.md)** — embed the loop in your own automation

---

# Getting Started

This tutorial walks through setting up ralphify, creating a ralph with commands, and running a productive autonomous loop. By the end, you'll have a self-healing coding loop that validates its own work.

## Prerequisites

- **Python 3.11+**
- **An AI coding agent CLI** — this tutorial uses [Claude Code](https://docs.anthropic.com/en/docs/claude-code). Install it with `npm install -g @anthropic-ai/claude-code`. Ralphify also works with [other agents](agents.md).
- **A project with a test suite** (we'll use this for the feedback loop)

## Step 1: Install ralphify

```bash
uv tool install ralphify
```

Alternative install methods

```bash
pip install ralphify   # pip works too
pipx install ralphify  # or pipx
```

Verify it's working:

```bash
ralph --version
```

```text
ralphify 0.3.0
```

## Step 2: Create a ralph

The fastest way to scaffold a ralph is `ralph scaffold`:

```bash
ralph scaffold my-ralph
```

```text
Created my-ralph/RALPH.md
Edit the file, then run: ralph run my-ralph
```

This creates `my-ralph/RALPH.md` with a ready-to-customize template including an example command, arg, and prompt. Edit the task section, [test it](#step-3-do-a-test-run), then follow [Step 4](#step-4-add-a-test-command) to add a test command — test feedback is what makes the loop self-healing.

Or create the file manually as shown below.

Installing an existing ralph?

Use [agr](https://github.com/computerlovetech/agr) to install shared ralphs from GitHub:

```bash
agr add owner/repo/my-ralph     # install a ralph from GitHub
ralph run my-ralph               # run it by name
```

### Manual setup

Create a ralph directory and `RALPH.md` with the agent field — this is the only required frontmatter:

```markdown
---
agent: claude -p --dangerously-skip-permissions
---

You are an autonomous coding agent running in a loop. Each iteration
starts with a fresh context. Your progress lives in the code and git.

Read TODO.md for the current task list. Pick the top uncompleted task,
implement it fully, then mark it done.

## Rules

- One task per iteration
- No placeholder code — full, working implementations only
- Run `uv run pytest -x` before committing
- Commit with a descriptive message like `feat: add X` or `fix: resolve Y`
- Mark the completed task in TODO.md
```

What does `--dangerously-skip-permissions` do?

Claude Code normally asks for your approval before running shell commands, editing files, or making git commits. The `--dangerously-skip-permissions` flag disables these interactive prompts so the agent can work autonomously without waiting for input. The `-p` flag enables non-interactive ("print") mode, which reads the prompt from stdin instead of opening a chat session.

## Step 3: Do a test run

Run a single iteration to verify your setup works:

```bash
ralph run my-ralph -n 1 --log-dir ralph_logs
```

This runs a single iteration and saves the output to `ralph_logs/`. Review the log to see what the agent did:

```bash
ls ralph_logs/
```

```text
001_20250115-142301.log
```

```bash
cat ralph_logs/001_*.log
```

Add `ralph_logs/` to `.gitignore`

Log files are useful for debugging but shouldn't be committed:

```bash
echo "ralph_logs/" >> .gitignore
```

If the agent produced useful work, you're ready to add test feedback.

Something not working?

If the agent errored or didn't do anything useful, check [Troubleshooting](troubleshooting.md) for common issues — agent hangs, missing commands, and frontmatter mistakes are all covered there.

## Step 4: Add a test command

Commands run each iteration and their output is injected into the prompt via `{{ commands. }}` placeholders. Add a test command and its placeholder:

```markdown
---
agent: claude -p --dangerously-skip-permissions
commands:
  - name: tests
    run: uv run pytest -x
---

# Prompt

## Test results

{{ commands.tests }}

You are an autonomous coding agent running in a loop. Each iteration
starts with a fresh context. Your progress lives in the code and git.

Read TODO.md for the current task list. Pick the top uncompleted task,
implement it fully, then mark it done.

If tests are failing, fix them before starting new work.

## Rules

- One task per iteration
- No placeholder code — full, working implementations only
- Commit with a descriptive message like `feat: add X` or `fix: resolve Y`
- Mark the completed task in TODO.md
```

The `tests` command runs `uv run pytest -x` each iteration. Its output replaces `{{ commands.tests }}` in the prompt, so the agent always sees the current test results. If tests fail, the agent fixes them — that's the self-healing loop.

## Step 5: Add more commands

Add a lint command and a git log for context:

```markdown
---
agent: claude -p --dangerously-skip-permissions
commands:
  - name: tests
    run: uv run pytest -x
  - name: lint
    run: uv run ruff check .
  - name: git-log
    run: git log --oneline -10
---

# Prompt

## Recent commits

{{ commands.git-log }}

## Test results

{{ commands.tests }}

## Lint results

{{ commands.lint }}

You are an autonomous coding agent running in a loop. Each iteration
starts with a fresh context. Your progress lives in the code and git.

Read TODO.md for the current task list. Pick the top uncompleted task,
implement it fully, then mark it done.

If tests or lint are failing, fix them before starting new work.

## Rules

- One task per iteration
- No placeholder code — full, working implementations only
- Commit with a descriptive message like `feat: add X` or `fix: resolve Y`
- Mark the completed task in TODO.md
```

## Step 6: Run the loop

Start with a few iterations to verify everything works:

```bash
ralph run my-ralph -n 3 --log-dir ralph_logs
```

Watch the output. Each iteration runs the commands, assembles the prompt with the command output, and pipes it to the agent:

```text
▶ Running: my-ralph
  3 commands · max 3 iterations

── Iteration 1 ──
  Commands: 3 ran
✓ Iteration 1 completed (45.2s)
  → ralph_logs/001_20250115-142301.log

── Iteration 2 ──
  Commands: 3 ran
✗ Iteration 2 failed with exit code 1 (23.1s)
  → ralph_logs/002_20250115-142512.log

── Iteration 3 ──
  Commands: 3 ran
✓ Iteration 3 completed (38.5s)
  → ralph_logs/003_20250115-142812.log

──────────────────────
Done: 3 iterations — 2 succeeded, 1 failed
```

The agent's output streams live to your terminal between the iteration markers — you can watch it work in real time. Press `p` to silence the stream if you prefer a quieter loop, and `p` again to resume.

If the agent breaks a test, the next iteration sees the failure output via `{{ commands.tests }}` and fixes it automatically.

Once you're confident the loop works, drop the `-n 3` to let it run indefinitely. Press `Ctrl+C` to stop.

## Step 7: Steer while it runs

The prompt body is re-read from disk every iteration. You can edit `RALPH.md` while the loop is running and the agent follows your changes on the next cycle.

When the agent does something you don't want, add a rule:

```markdown
## Rules

- Do NOT delete failing tests — fix the underlying code instead
```

When you want to shift focus, change the task:

```markdown
Read TODO.md and focus only on the API module.
```

This is the most powerful part of ralph loops — you're steering a running agent with a text file.

Frontmatter changes need a restart

Only the **prompt body** is re-read each iteration. Frontmatter fields (`agent`, `commands`, `args`) are parsed once at startup. If you add a new command or change the agent, stop the loop with `Ctrl+C` and restart it.

## Next steps

- [Cookbook](cookbook.md) — copy-pasteable setups for coding, docs, research, and more
- [How it Works](how-it-works.md) — what happens inside each iteration
- [Troubleshooting](troubleshooting.md) — when things don't work as expected
- [CLI Reference](cli.md) — all commands and options

---

# How the ralph loop works

TL;DR

Each iteration: re-read `RALPH.md` from disk → run commands → replace `{{ placeholders }}` with output → pipe the assembled prompt to the agent → agent works and exits → repeat. The prompt body is re-read every iteration (so you can edit it live), but frontmatter is parsed once at startup. Failed commands still capture output — that's what makes the loop self-healing.

What happens inside each iteration of an autonomous coding loop? This page breaks down the lifecycle — from command execution to prompt assembly to agent piping — so you can write better prompts, debug unexpected behavior, and understand the self-healing feedback cycle.

## The six steps of each iteration

Every iteration follows the same sequence. Here's what happens at each step.

### 1. Re-read the prompt from disk

The prompt body (everything below the frontmatter) is read from disk **every iteration**. This means you can edit the prompt text — add rules, change the task, adjust constraints — while the loop is running. Changes take effect on the next cycle.

Frontmatter fields (`agent`, `commands`, `args`) are parsed once at startup. To change those, restart the loop.

### 2. Run commands and capture output

Each command defined in the `commands` frontmatter runs in order and captures its output (stdout + stderr). Commands run from the **project root** by default. Commands starting with `./` run relative to the **ralph directory** instead — useful for scripts bundled alongside your `RALPH.md`.

Command output is captured **regardless of exit code** — a command like `pytest -x` exits non-zero when tests fail, but its output is exactly what you want in the prompt.

Each command has a default timeout of 60 seconds. If a command takes longer, the process is killed and the output captured so far is used. Override this with the `timeout` field for slow commands (e.g., a large test suite):

```yaml
commands:
  - name: tests
    run: uv run pytest -x
    timeout: 300  # 5 minutes
```

### 3. Resolve placeholders with command output

Each `{{ commands. }}` placeholder in the prompt body is replaced with the corresponding command's output. **Only commands referenced by a placeholder appear in the prompt** — if you define a command but don't use `{{ commands. }}` for it, the command still runs every iteration but its output is excluded. This forces you to place data deliberately rather than accidentally dumping everything into the prompt.

Placeholders for `{{ args. }}` are replaced with user argument values from the CLI — both in the prompt body and in command `run` strings.

`{{ ralph.* }}` placeholders provide runtime metadata — no frontmatter configuration needed:

- `{{ ralph.name }}` — the ralph's directory name (e.g. `my-ralph`)
- `{{ ralph.iteration }}` — current iteration number (1-based)
- `{{ ralph.max_iterations }}` — total iterations if `-n` was set, empty otherwise

These are useful for iteration-aware prompts — for example, telling the agent to wrap up on the last iteration, or including the ralph name in commit messages.

Unmatched placeholders resolve to an empty string — you won't see raw `{{ }}` text in the assembled prompt.

### 4. Assemble the final prompt

The prompt body (everything below the YAML frontmatter in `RALPH.md`) with all placeholders resolved becomes the fully assembled prompt — a single text string ready for the agent.

HTML comments (``) are stripped during assembly — they never reach the agent. Use them for notes to yourself, like why a rule exists or what to change next.

By default, ralphify appends a **co-author trailer instruction** to the end of the prompt, asking the agent to include `Co-authored-by: Ralphify ` in its commit messages. This gives visibility into which commits were produced by a ralph loop. To disable it, set `credit: false` in the frontmatter.

### 5. Pipe the prompt to the AI agent

The assembled prompt is piped to the agent command via stdin:

```bash
echo "<assembled prompt>" | claude -p --dangerously-skip-permissions
```

The agent reads the prompt, does work in the current directory (edits files, runs commands, makes commits), and exits. Ralphify waits for the agent process to finish.

When the agent command starts with `claude`, ralphify automatically adds `--output-format stream-json --verbose` to enable structured streaming. This lets ralphify track agent activity in real time — you don't need to configure this yourself.

### 6. Loop back with fresh context

The loop starts the next iteration from step 1. The RALPH.md is re-read, commands run again with fresh output, and the agent gets a new prompt reflecting the current state of the codebase.

## What gets re-read vs. what stays fixed

| What | When read | Why it matters |
|---|---|---|
| Prompt body | Every iteration | Edit the prompt while the loop runs — the next iteration follows your new instructions |
| Command output | Every iteration | The agent always sees fresh data (latest git log, current test status, etc.) |
| Frontmatter (`agent`, `commands`, `args`) | Once at startup | Parsed when the loop starts. Restart to pick up changes. |
| User arguments | Once at startup | Passed via CLI flags, constant for the run |

## How broken code gets fixed automatically

Here's a concrete example. Given this `RALPH.md`:

```markdown
---
agent: claude -p --dangerously-skip-permissions
commands:
  - name: git-log
    run: git log --oneline -5
  - name: tests
    run: uv run pytest -x
---

# Prompt

## Recent commits

{{ commands.git-log }}

## Test results

{{ commands.tests }}

Read TODO.md and implement the next task.
If tests are failing, fix them first.
```

### Iteration 1 (tests passing)

The assembled prompt piped to the agent:

```markdown
# Prompt

## Recent commits

a1b2c3d feat: add user model
e4f5g6h fix: resolve database connection timeout
i7j8k9l docs: update API reference

## Test results

============================= 5 passed in 1.23s ==============================

Read TODO.md and implement the next task.
If tests are failing, fix them first.
```

### Iteration 2 (tests broken by iteration 1)

```markdown
# Prompt

## Recent commits

x1y2z3a feat: add login endpoint
a1b2c3d feat: add user model
e4f5g6h fix: resolve database connection timeout

## Test results

FAILED tests/test_auth.py::test_login - AssertionError: expected 200, got 401
============================= 1 failed, 4 passed in 1.45s ====================

Read TODO.md and implement the next task.
If tests are failing, fix them first.
```

The agent sees the test failure and the instruction to fix it first. This is the **self-healing feedback loop**: the agent breaks something, the command captures the failure, and the agent sees it in the next iteration.

## Command execution order and failure handling

Commands run in the order they appear in the `commands` list in the RALPH.md frontmatter. All commands run regardless of whether earlier commands fail.

```yaml
commands:
  - name: tests      # Runs first
    run: uv run pytest -x
  - name: lint       # Runs second
    run: uv run ruff check .
  - name: git-log    # Runs third
    run: git log --oneline -10
```

## How to stop the loop (Ctrl+C, limits, and errors)

The loop continues until one of these happens:

| Condition | What happens |
|---|---|
| `Ctrl+C` (first) | Gracefully finishes the current iteration, then stops the loop. The agent completes its work and the iteration result is recorded. |
| `Ctrl+C` (second) | Force-stops immediately — kills the agent process and exits. Use when you don't want to wait for the current iteration to finish. |
| `-n` limit reached | Loop stops after completing the specified number of iterations |
| `--stop-on-error` and agent exits non-zero or times out | Loop stops after the current iteration |
| `--timeout` exceeded | Agent process is killed, iteration is marked as timed out, loop continues (unless `--stop-on-error`) |

## Next steps

- [Getting Started](getting-started.md) — set up your first loop
- [CLI Reference](cli.md) — all commands and options
- [Troubleshooting](troubleshooting.md) — when things don't work as expected

---

# Using with Different Agents

Ralphify works with **any CLI that reads a prompt from stdin and exits when done**. Claude Code is the default, but you can use any tool that follows this contract.

This page shows how to configure the `agent` field in your RALPH.md for popular agents and how to write your own wrapper.

## Agent comparison

| Agent | Stdin support | Streaming | Wrapper needed |
|---|---|---|---|
| [Claude Code](#claude-code) | Native (`-p`) | Yes — real-time activity tracking | No |
| [Aider](#aider) | Via bash wrapper | No | Yes (`bash -c`) |
| [Codex CLI](#codex-cli) | Native (`exec`) | No | No |
| [Custom](#custom-wrapper-script) | You implement it | No | Yes (script) |

If you're not sure which to pick: **start with Claude Code.** It has the deepest integration, the best autonomous coding capabilities, and is the default.

## What ralphify needs from an agent

Every iteration, ralphify runs your agent like this:

```bash
echo "<assembled prompt>" | <agent command>
```

Your agent must:

1. **Read a prompt from stdin** — the full assembled prompt is piped in
2. **Do work in the current directory** — edit files, run commands, make commits
3. **Exit when done** — exit code 0 means success, non-zero means failure

That's it. No special protocol, no API — just stdin in, work done, process exits.

## Claude Code

The default and recommended agent.

```markdown
---
agent: claude -p --dangerously-skip-permissions
---
```

| Flag | Purpose |
|---|---|
| `-p` | Non-interactive mode — reads prompt from stdin, prints output, exits |
| `--dangerously-skip-permissions` | Skips approval prompts so the agent can work autonomously |

Install Claude Code:

```bash
npm install -g @anthropic-ai/claude-code
```

Why `--dangerously-skip-permissions`?

Without this flag, Claude Code pauses to ask for approval before editing files, running commands, or making commits. In an autonomous loop, nobody is there to approve — so the agent would hang forever. Commands in your RALPH.md act as your guardrails instead.

### Automatic streaming mode

When ralphify detects that the agent command starts with `claude`, it automatically adds `--output-format stream-json --verbose` to the command. You don't need to add these flags yourself.

This enables ralphify to:

- Parse Claude Code's structured JSON output line by line
- Track agent activity in real time
- Extract the final result text from the agent's response

## Aider

[Aider](https://aider.chat) is an AI pair-programming tool that works with multiple LLM providers.

```markdown
---
agent: bash -c 'aider --yes-always --no-auto-commits --message "$(cat -)"'
---
```

| Flag | Purpose |
|---|---|
| `--yes-always` | Auto-approve all changes (no interactive prompts) |
| `--no-auto-commits` | Let your prompt control when commits happen |
| `--message "..."` | Pass the prompt as a message instead of stdin |

Why the bash wrapper?

Aider doesn't natively read prompts from stdin. The `bash -c` wrapper reads stdin with `cat -` and passes it as a `--message` argument.

### Aider with a specific model

```markdown
---
agent: bash -c 'aider --yes-always --no-auto-commits --model claude-sonnet-4-6 --message "$(cat -)"'
---
```

## Codex CLI

[OpenAI Codex CLI](https://github.com/openai/codex) supports non-interactive use natively via its `exec` subcommand.

```markdown
---
agent: codex exec --sandbox danger-full-access -
---
```

| Flag | Purpose |
|---|---|
| `exec` | Non-interactive mode — designed for piped/scripted use |
| `--sandbox danger-full-access` | Full filesystem access for autonomous operation |
| `-` | Read prompt from stdin |

## Custom wrapper script

For full control, write a wrapper script that reads stdin and calls your agent however it needs to be called.

**`ralph-agent.sh`**

```bash
#!/bin/bash
set -e

# Read the prompt from stdin
PROMPT=$(cat -)

# Call your agent however it works
my-custom-agent --input "$PROMPT" --auto-approve
```

```bash
chmod +x ralph-agent.sh
```

**`my-ralph/RALPH.md`**

```markdown
---
agent: ./ralph-agent.sh
---

Your prompt here.
```

## Testing your setup

Verify the agent works outside of ralphify first. The command depends on which agent you're using:

    ```bash
    echo "Say hello and nothing else" | claude -p --dangerously-skip-permissions
    ```

    ```text
    Hello!
    ```

    ```bash
    echo "Say hello and nothing else" | bash -c 'aider --yes-always --no-auto-commits --message "$(cat -)"'
    ```

    ```bash
    echo "Say hello and nothing else" | codex exec --sandbox danger-full-access -
    ```

If the agent prints a response and exits, your setup is working. If it hangs or errors, fix the agent installation before continuing.

Then test through ralphify with a single iteration:

```bash
ralph run my-ralph -n 1 --log-dir ralph_logs
```

Non-Claude-Code agents

Disable auto-commits if your prompt handles commits — most agents have this feature, and it conflicts with prompt-driven commit instructions. Use `--timeout` as a safety net in case the agent enters an unexpected interactive mode.

## How agent output works

Ralphify uses two different execution modes depending on the agent:

### Streaming mode (Claude Code)

When the agent command starts with `claude`, ralphify spawns the process with `Popen` and reads its JSON output line by line. This enables:

- **Live activity tracking** — the terminal shows what the agent is doing in real time
- **Result text extraction** — the agent's final response is captured
- **Verbose logging** — with `--log-dir`, logs include the agent's internal tool calls and reasoning

### Blocking mode (all other agents)

For non-Claude agents, ralphify uses `subprocess.run` and waits for the process to exit. Output behavior depends on whether you're using `--log-dir`:

- **Without `--log-dir`** — agent output streams directly to your terminal in real time
- **With `--log-dir`** — output is captured, written to a log file, then replayed to the terminal after the iteration completes

Both modes support all ralphify features (commands, timeouts, iteration tracking). The difference is only in how output is handled during each iteration.

## Adapting other tools

Many AI coding tools don't read from stdin directly but can be adapted with a bash wrapper. The pattern is:

```bash
bash -c '<tool> <auto-approve-flag> --message "$(cat -)"'
```

The `cat -` reads the piped prompt from stdin and passes it as a command-line argument. This works for any tool that accepts a prompt via a flag (like `--message`, `--input`, `--prompt`).

If the tool has no way to accept a prompt non-interactively, a [custom wrapper script](#custom-wrapper-script) is the escape hatch — you can use the prompt text however the tool needs it.

## Next steps

- [Getting Started](getting-started.md) — set up your first loop with the agent you just configured
- [Troubleshooting](troubleshooting.md) — when the agent hangs, produces no output, or exits unexpectedly

---

# Cookbook

Copy-pasteable setups for common autonomous workflows. Each recipe is a real, runnable ralph from the [`examples/`](https://github.com/computerlovetech/ralphify/tree/main/examples) directory.

All recipes use **Claude Code** as the agent. To use a different agent, swap the `agent` field — see [Using with Different Agents](agents.md).

User arguments in recipes

Many recipes accept CLI arguments like `--focus` or `--target`. These aren't built-in flags — they're **user arguments** declared in each recipe's `args` field. When you pass `--focus "test coverage"`, the value replaces `{{ args.focus }}` in the prompt. See [User Arguments](cli.md#user-arguments) for details.

---

## Run autonomous ML experiments 

An autonomous ML research loop inspired by [Karpathy's autoresearch](https://github.com/karpathy/autoresearch). The agent runs experiments on a training script to minimize validation loss — modifying code, training for 5 minutes, keeping improvements and reverting failures.

**`autoresearch/RALPH.md`**

```markdown
---
agent: claude -p --dangerously-skip-permissions
commands:
  - name: results
    run: ./show-results.sh
  - name: git-log
    run: git log --oneline -20
  - name: last-run
    run: ./show-last-run.sh
args:
  - train_script
  - prepare_script
credit: false
---

# Autoresearch

You are an autonomous ML research agent running in a loop. Each iteration starts with a fresh context. Your progress lives in `results.tsv` and git history.

Your job: run experiments on `{{ args.train_script }}` to minimize **val_bpb** (validation bits per byte). Each training run uses a fixed 5-minute time budget, so all experiments are directly comparable.

## State

### Experiment history

{{ commands.results }}

### Git log

{{ commands.git-log }}

### Last run output

{{ commands.last-run }}

## Files

- **`{{ args.prepare_script }}`** — fixed constants, data prep, tokenizer, dataloader, evaluation. **Do not modify.**
- **`{{ args.train_script }}`** — the only file you edit. Model architecture, optimizer, hyperparameters, training loop. Everything is fair game.

Read both files at the start of each iteration to understand the current state.

## The experiment loop

Each iteration, do exactly one experiment:

1. **Orient** — review the experiment history and git log above. Identify what has been tried, what worked, what failed. Identify the current best val_bpb.
2. **Hypothesize** — pick ONE idea to test. Consider: architecture changes, optimizer tuning, hyperparameters, batch size, model size, activation functions, attention patterns, etc.
3. **Implement** — edit `{{ args.train_script }}` with your change.
4. **Commit** — `git commit` your change with a short descriptive message.
5. **Run** — execute: `uv run {{ args.train_script }} > run.log 2>&1` (redirect everything, do NOT let output flood your context).
6. **Read results** — `grep "^val_bpb:\|^peak_vram_mb:" run.log`. If empty, the run crashed; run `tail -n 50 run.log` to diagnose.
7. **Record** — append results to `results.tsv` (tab-separated). Do NOT commit results.tsv.
8. **Decide**:
   - val_bpb **improved** (lower): keep the commit, the branch advances.
   - val_bpb **equal or worse**: `git reset --hard HEAD~1` to revert.
   - **Crash**: log as crash in results.tsv, revert. If it's a trivial fix (typo, missing import), fix and retry once.

<!-- The first iteration should run the train script unmodified to establish the baseline. -->

## results.tsv format

Tab-separated, 5 columns:

```
commit	val_bpb	memory_gb	status	description
```

- commit: short hash (7 chars)
- val_bpb: e.g. 0.997900 (use 0.000000 for crashes)
- memory_gb: peak_vram_mb / 1024, rounded to .1f (use 0.0 for crashes)
- status: `keep`, `discard`, or `crash`
- description: short text of what was tried

If `results.tsv` doesn't exist yet, create it with just the header row, then run the baseline.

## Rules

- ONE experiment per iteration. No multi-variable changes.
- **Never modify `{{ args.prepare_script }}`**. It is read-only.
- **No new dependencies**. Only use what's in `pyproject.toml`.
- **Simplicity criterion**: a small val_bpb gain that adds ugly complexity is not worth it. Removing code for equal or better results is a win.
- **VRAM** is a soft constraint. Some increase is acceptable for meaningful val_bpb gains, but don't blow it up.
- **Timeout**: if a run exceeds 10 minutes, kill it and treat as crash.
- Never ask the human for input. You are fully autonomous.
```

The helper scripts surface experiment state each iteration:

**`autoresearch/show-results.sh`**

```bash
#!/bin/bash
# Show experiment history from results.tsv
if [ -f results.tsv ]; then
    cat results.tsv
else
    echo "No results.tsv yet — first iteration should create it and run the baseline."
fi
```

**`autoresearch/show-last-run.sh`**

```bash
#!/bin/bash
# Show key metrics from the most recent training run
if [ -f run.log ]; then
    grep "^val_bpb:\|^training_seconds:\|^peak_vram_mb:\|^mfu_percent:\|^num_params_M:\|^depth:" run.log
else
    echo "No run.log yet — no training runs have been executed."
fi
```

```bash
ralph run autoresearch --train_script train.py --prepare_script prepare.py
```

The `train_script` and `prepare_script` args let you point the ralph at any autoresearch-style project. The agent handles everything autonomously: establishing a baseline on the first iteration, then running experiments indefinitely. Each iteration is one hypothesis tested — modify the train script, train, evaluate, keep or revert.

---

## Improve code quality continuously 

A loop that continuously improves code quality without changing functionality. It runs tests, type checking, and lint each iteration, then picks one improvement to make.

**`improve-codebase/RALPH.md`**

```markdown
---
agent: claude -p --dangerously-skip-permissions
commands:
  - name: tests
    run: uv run pytest -x
  - name: types
    run: uv run ty check
  - name: lint
    run: uv run ruff check .
  - name: git-log
    run: git log --oneline -10
args:
  - focus
---

# Improve Codebase

You are an autonomous coding agent running in a loop. Each iteration
starts with a fresh context. Your progress lives in the code and git.

## Recent changes

{{ commands.git-log }}

## Test results

{{ commands.tests }}

If any tests are failing above, fix them before doing anything else.

## Type checking

{{ commands.types }}

## Lint

{{ commands.lint }}

Fix any type errors or lint violations above before making new changes.

## Task

Make improvements to this codebase without changing any functionality.
{{ args.focus }}

Pick one improvement per iteration from the categories below (or discover your own). Research the code before changing anything.

## Improvement categories

### Code Quality
- Remove dead code, unused imports, and unreachable branches
- Eliminate code duplication by extracting shared logic into reusable functions
- Replace magic numbers and hardcoded strings with named constants
- Simplify overly complex conditionals and nested logic

### Structure & Organization
- Break up large files or functions that are doing too many things
- Move code to more logical locations (wrong file, wrong module, wrong layer)
- Standardize inconsistent naming conventions across the codebase
- Group related functionality that is scattered across unrelated files

### Robustness
- Add missing error handling and edge case coverage
- Replace silent failures with meaningful errors or logs
- Harden functions that assume inputs are always valid

### Readability
- Add or improve inline comments for non-obvious logic
- Improve variable and function names that are vague or misleading
- Normalize inconsistent formatting, spacing, or style

### Tests
- Increase test coverage for untested or undertested modules
- Remove flaky, redundant, or low-value tests
- Improve test naming so failures are self-explanatory

### Dependencies & Config
- Remove unused dependencies
- Consolidate duplicated configuration
- Replace deprecated library usage with modern equivalents

This is not an exhaustive list. If you discover opportunities for improving the codebase while not changing functionality, go for it!

## Rules

- One improvement per iteration
- Research code before creating anything new
- No placeholder code — full, working implementations only
- Fix all test failures, type errors, and lint violations before committing
- Commit with a descriptive message and push
```

```bash
ralph run improve-codebase -n 10 --log-dir ralph_logs
ralph run improve-codebase -n 5 --focus "focus on test coverage"
```

---

## Write and improve documentation automatically 

A loop focused on writing and improving documentation.

**`docs/RALPH.md`**

```markdown
---
agent: claude -p --dangerously-skip-permissions
commands:
  - name: docs-build
    run: uv run mkdocs build --strict
  - name: git-log
    run: git log --oneline -20
  - name: git-diff
    run: git diff --name-only HEAD~10
args:
  - focus
---

# Docs

You are an autonomous coding agent running in a loop. Each iteration
starts with a fresh context. Your progress lives in the code and git.

## Docs build output

{{ commands.docs-build }}

If there are warnings or errors above, fix them first.

## Recent changes

{{ commands.git-log }}

## Recently changed files

{{ commands.git-diff }}

## Task

Maintain and improve the ralphify documentation.
{{ args.focus }}

Pick one thing per iteration. Read the relevant code and existing docs
before making any change.

## What the docs are for

The docs serve two jobs:

1. **Users who want to build cool ralphs** — they need to understand
   the ralph format, prompt patterns, CLI flags, and how to get the
   most out of the loop. Get them productive fast.

2. **Project growth** — people discovering ralphify need to immediately
   understand what it does, why it matters, and how to get started.
   First impressions count: landing page, README, SEO, and visual
   polish all matter here.

Contributor docs (`docs/contributing/`) help developers and coding
agents understand the codebase so they can contribute effectively.

## Principles

- **Don't over-document.** Only document what helps someone do
  something. If it's obvious from the CLI help or the code, skip it.
  Not every function or flag needs a docs page.

- **Don't gold-plate.** Good enough is good enough. Clean, correct,
  and scannable beats comprehensive and polished. Move on.

- **Close important gaps.** When recent code changes introduced new
  features or changed behavior, update the relevant doc surfaces.
  Not every change needs a docs update — use judgement.

- **Keep all surfaces in sync.** When something user-facing changes,
  check: `docs/`, `README.md`, `src/ralphify/skills/new-ralph/SKILL.md`,
  and `docs/quick-reference.md`. Update what's relevant.

- **SEO basics.** Every page should have a clear `description` and
  `keywords` in its frontmatter. Titles should be descriptive. Don't
  over-optimize — just make sure search engines can understand what
  each page is about.

- **Branding and feel.** Ralphify's tone is direct, casual, and
  practical. The visual identity uses violet/deep-purple (#8B6CF0)
  and orange (#E87B4A) as brand colors. Keep the look consistent
  with existing pages. Don't introduce new visual patterns without
  good reason.

- **Think jobs-to-be-done**, not feature lists. Frame docs around
  what the user is trying to accomplish: "How do I pass arguments to
  my ralph?" not "The args field accepts a list of strings."

## What to work on

Look at the recent commits and changed files above. Then:

1. Fix any mkdocs build warnings or errors
2. Close gaps between code changes and docs
3. Improve existing pages (clarity, examples, scannability)
4. Improve SEO metadata where it's missing or weak
5. Clean up anything that feels bloated or gold-plated

## Rules

- One improvement per iteration
- Read the code and existing docs before changing anything
- Run `mkdocs build --strict` and ensure zero warnings before committing
- Commit with a descriptive message and push
```

```bash
ralph run docs --log-dir ralph_logs
ralph run docs --focus "focus on the API reference pages"
```

---

## Find and fix bugs automatically 

A loop that discovers bugs and fixes them. The agent reads the codebase, finds a real bug (edge case, off-by-one, missing validation), writes a failing test to prove it, then fixes it.

**`bug-hunter/RALPH.md`**

```markdown
---
agent: claude -p --dangerously-skip-permissions
commands:
  - name: tests
    run: uv run pytest -x
  - name: types
    run: uv run ty check
  - name: lint
    run: uv run ruff check .
  - name: git-log
    run: git log --oneline -10
args:
  - focus
---

# Bug Hunter

You are an autonomous bug-hunting agent running in a loop. Each
iteration starts with a fresh context. Your progress lives in the
code and git.

## Test results

{{ commands.tests }}

## Type checking

{{ commands.types }}

## Lint

{{ commands.lint }}

## Recent commits

{{ commands.git-log }}

If tests, types, or lint are failing, fix that before hunting for new bugs.

## Task

Find and fix a real bug in this codebase.
{{ args.focus }}

Each iteration:

1. **Read code** — pick a module and read it carefully. Look for
   edge cases, off-by-one errors, missing validation, incorrect
   error handling, race conditions, or logic errors.
2. **Write a failing test** — prove the bug exists with a test that
   fails on the current code.
3. **Fix the bug** — make the test pass with a minimal fix.
4. **Verify** — all existing tests must still pass.

## Rules

- One bug per iteration
- The bug must be real — do not invent hypothetical issues
- Always write a regression test before fixing
- Do not change unrelated code
- Commit with `fix: resolve <description>`
```

```bash
ralph run bug-hunter -n 10 --log-dir ralph_logs
ralph run bug-hunter -n 5 --focus "focus on input validation"
```

---

## Run structured AI research loops 

A structured research loop that builds up a report over many iterations. Uses shell scripts as commands to track maturity, show the question tree, and even run an editorial review agent that gives feedback between iterations.

This is a more advanced ralph — it uses `args` for the research topic, helper scripts (run with `./` relative to the ralph directory), and a `timeout` on the review command.

**`research/RALPH.md`**

```markdown
---
agent: claude -p --dangerously-skip-permissions
commands:
  - name: git-log
    run: git log --oneline -15
  - name: last-diff
    run: git diff --stat HEAD~1
  - name: scratchpad
    run: ./show-focus.sh
  - name: questions
    run: ./show-questions.sh
  - name: outline
    run: ./show-outline.sh
  - name: maturity
    run: ./show-maturity.sh
  - name: review
    run: ./review.sh
    timeout: 120
args:
  - workspace
  - focus
---

# Deep Research

You are an autonomous research agent running in a loop. Each iteration starts with a fresh context. Your progress lives in files and git history.

## Your mission

{{ args.focus }}

Conduct structured, iterative research on this topic. Go deep. Discover angles and insights that aren't obvious from the surface.

## State

### Editorial review

{{ commands.review }}

Pay close attention to the review above. It's written by an editor who can see your full body of work. Follow its guidance on where to focus and what to improve.

### Git history (your progress across iterations)

{{ commands.git-log }}

### What changed last iteration

{{ commands.last-diff }}

### Last scratchpad entry

{{ commands.scratchpad }}

### Research questions

{{ commands.questions }}

### Report outline

{{ commands.outline }}

### Research maturity

{{ commands.maturity }}

## Workspace

You work within `{{ args.workspace }}/`. Read `{{ args.workspace }}/CONVENTIONS.md` for the full workspace structure and formatting rules. The short version:

- `REPORT.md` — executive overview + chapter table of contents (keep under 150 lines)
- `chapters/NN-slug.md` — deep-dive chapter files
- `notes/` — working memory: `questions.md`, `sources.md`, `insights.md`, `scratchpad.md`

If the workspace doesn't exist yet, create it and populate from the conventions file.

## Each iteration

1. **Orient** — read the state above. Read the editorial review. Understand where you left off.

2. **Decide: research or refine?** Roughly every 3-4 iterations, skip research and instead tighten prose, merge overlapping sections, restructure chapters, and sharpen insights. Less but better content always wins. Write your decision and focus area to `notes/scratchpad.md` before starting.

3. **Research** — pick ONE question or area. Go deep. Use web search aggressively. Prioritize practitioner sources (engineering blogs, HN/Reddit discussions, conference talks, RFCs) over generic SEO content. Parallelize across sub-agents when surveying a broad area. Log every useful source in `notes/sources.md`.

4. **Capture** — update `notes/questions.md` (mark answered, add new), add insights to `notes/insights.md`, dump raw notes to `notes/scratchpad.md`.

5. **Write** — findings go into the appropriate chapter. Best insights get distilled up into REPORT.md. Keep REPORT.md as a table of contents that links to chapters — don't inline detail.

6. **Commit and push** — stage all changes in `{{ args.workspace }}/`, commit, push.

## Rules

- ONE focused thread per iteration. Depth over breadth.
- The research question tree (`notes/questions.md`) must grow every research iteration.
- Every web source gets logged in `notes/sources.md` with URL, author, one-line summary, and relevance rating.
- The report should be readable and valuable at any point, not just at the end.
- Do not fabricate sources. When you find contradictions, note both sides.
- Prefer concrete examples and practical implications over abstract theory.
```

The helper scripts (`show-focus.sh`, `show-questions.sh`, etc.) read from the workspace files and surface key state. The `review.sh` script pipes the full workspace to a separate Claude call that acts as an editorial reviewer — giving the research agent targeted feedback each iteration.

```bash
ralph run research --workspace ai-safety --focus "current approaches to AI alignment"
```

This recipe shows several advanced patterns: commands that call scripts relative to the ralph directory (`./show-focus.sh`), a command with a `timeout`, a command that itself calls an AI agent (`review.sh` pipes to `claude -p`), and `args` used in the prompt body via `{{ args.workspace }}` placeholders.

---

## Migrate code patterns across a codebase 

A loop for batch code transformations — migrating from one pattern to another across a codebase. The `remaining` command counts how many files still need migration, giving the agent a clear finish line.

**`migrate/RALPH.md`**

```markdown
---
agent: claude -p --dangerously-skip-permissions
commands:
  - name: tests
    run: uv run pytest -x
  - name: remaining
    run: ./count-remaining.sh {{ args.old_pattern }}
  - name: types
    run: uv run ty check
  - name: lint
    run: uv run ruff check .
  - name: git-log
    run: git log --oneline -10
args:
  - old_pattern
  - new_pattern
---

# Code Migration

You are an autonomous coding agent running in a loop. Each iteration
starts with a fresh context. Your progress lives in the code and git.

## Migration spec

Migrate all usages of `{{ args.old_pattern }}` to `{{ args.new_pattern }}`.

## Remaining files

{{ commands.remaining }}

## Test results

{{ commands.tests }}

## Type checking

{{ commands.types }}

## Lint

{{ commands.lint }}

## Recent commits

{{ commands.git-log }}

If tests, types, or lint are failing, fix them before migrating more files.

## Rules

- Migrate 1-3 files per iteration — small batches that stay green
- Run tests after each change to catch breakage early
- Do not change behavior — only update the pattern
- Commit with `refactor: migrate <file> from old_pattern to new_pattern`
- If a file needs more than a mechanical replacement, note it in
  MIGRATION_NOTES.md and skip it
```

The `count-remaining.sh` script receives the pattern as an argument (resolved from `{{ args.old_pattern }}` in the `run` field) to find files that still need migration:

```bash
#!/bin/bash
pattern="$1"
files=$(grep -rl "$pattern" src/ 2>/dev/null)
count=$(echo "$files" | grep -c . 2>/dev/null || echo 0)
echo "$count files remaining"
echo "$files" | head -20
```

```bash
ralph run migrate --old_pattern "from utils import legacy_helper" \
                  --new_pattern "from core.helpers import modern_helper"
```

The `remaining` command gives the agent a shrinking counter and a list of files still needing attention, so it always knows where to focus next.

---

## Automate security scanning and fixes 

An iterative security review loop. The agent runs a scanner each iteration, picks one finding, fixes it, and verifies the fix. Good for systematically hardening a codebase.

**`security/RALPH.md`**

```markdown
---
agent: claude -p --dangerously-skip-permissions
commands:
  - name: scan
    run: uv run bandit -r src/ -f json
  - name: open-issues
    run: cat SECURITY_FINDINGS.md
  - name: tests
    run: uv run pytest -x
  - name: types
    run: uv run ty check
  - name: lint
    run: uv run ruff check .
  - name: git-log
    run: git log --oneline -10
---

# Security Scan

You are an autonomous security agent running in a loop. Each iteration
starts with a fresh context. Your progress lives in the code and git.

## Scanner results

{{ commands.scan }}

## Open findings

{{ commands.open-issues }}

## Test results

{{ commands.tests }}

## Type checking

{{ commands.types }}

## Lint

{{ commands.lint }}

## Recent commits

{{ commands.git-log }}

If tests, types, or lint are failing, fix them before addressing security findings.

## Task

Review the scanner results above. Pick one finding and fix it. If a
finding is a false positive, document why in SECURITY_FINDINGS.md and
mark it as dismissed.

If no scanner findings remain, do a manual review: read one module,
look for injection risks, auth bypasses, or unsafe data handling, and
fix or document what you find.

## Rules

- One finding per iteration
- Always verify the fix doesn't break tests
- Log every finding (fixed or dismissed) in SECURITY_FINDINGS.md
  with: severity, location, description, resolution
- Do not suppress scanner warnings — fix the underlying issue
- Commit with `security: fix <description>`
```

```bash
ralph run security -n 10 --log-dir ralph_logs
```

Swap `bandit` for your scanner of choice — `semgrep`, `npm audit`, `cargo audit`, etc. The pattern works the same: scan, pick a finding, fix it, log it.

---

## Increase test coverage automatically 

A loop that systematically increases test coverage. The agent sees the current coverage percentage and a list of uncovered functions, then writes tests for one module per iteration.

**`test-coverage/RALPH.md`**

```markdown
---
agent: claude -p --dangerously-skip-permissions
commands:
  - name: coverage
    run: uv run pytest --cov=src --cov-report=term-missing -q
  - name: types
    run: uv run ty check
  - name: lint
    run: uv run ruff check .
  - name: git-log
    run: git log --oneline -10
args:
  - target
---

# Test Coverage

You are an autonomous testing agent running in a loop. Each iteration
starts with a fresh context. Your progress lives in the code and git.

## Current coverage

{{ commands.coverage }}

## Lint

{{ commands.lint }}

## Recent commits

{{ commands.git-log }}

## Type checking

{{ commands.types }}

Fix any type errors or lint violations above before writing new tests.

## Task

Increase test coverage for this project.
{{ args.target }}

Pick the module with the most missing lines from the coverage report
above. Read the source code, understand what it does, and write
meaningful tests that exercise the uncovered paths.

## Rules

- One module per iteration
- Write tests that verify behavior, not just hit lines — assert
  return values, side effects, and error cases
- Do not mock things unnecessarily — prefer real objects when feasible
- Do not add `# pragma: no cover` comments
- All existing tests must still pass after your changes
- Commit with `test: add coverage for <module>`
```

```bash
ralph run test-coverage -n 10 --log-dir ralph_logs
ralph run test-coverage -n 5 --target "focus on error handling paths"
```

The coverage report gives the agent a clear metric to improve and shows exactly which lines are missing, so it always knows where to focus.

---

## Next steps

- [CLI Reference](cli.md) — all `ralph run` options (`--timeout`, `--stop-on-error`, `--delay`, user args)
- [Troubleshooting](troubleshooting.md) — when the agent hangs, commands fail, or output looks wrong

---

# Quick Reference

Everything you need at a glance. Bookmark this page.

## CLI commands

```bash
ralph run my-ralph                 # Run loop forever (Ctrl+C to stop)
ralph run my-ralph/RALPH.md        # Can also pass the file path directly
ralph run my-ralph -n 5            # Run 5 iterations
ralph run my-ralph -n 1 --log-dir logs  # Single iteration with output capture
ralph run my-ralph --stop-on-error # Stop if agent exits non-zero or times out
ralph run my-ralph --delay 10      # Wait 10s between iterations
ralph run my-ralph --timeout 300   # Kill agent after 5 min per iteration
ralph run my-ralph --dir ./src     # Pass user args to the ralph

ralph scaffold my-task              # Scaffold a ralph from template
ralph scaffold                     # Scaffold in current directory

ralph --version                    # Show version
```

Full flag descriptions and examples → [CLI reference](cli.md)

## Directory structure

```text
my-ralph/
└── RALPH.md              # Prompt + configuration (required)
```

That's it. A ralph is a directory with a `RALPH.md` file. See [Getting Started](getting-started.md) to create your first one.

## RALPH.md format

```markdown
---
agent: claude -p --dangerously-skip-permissions # (1)!
commands: # (2)!
  - name: tests
    run: uv run pytest -x
  - name: lint
    run: uv run ruff check .
  - name: git-log
    run: git log --oneline -10
args: [dir, focus] # (3)!
---

# Prompt body  <!-- (4) -->

{{ commands.git-log }} <!-- (5) -->

{{ commands.tests }}

{{ commands.lint }}

Your instructions here. Use {{ args.dir }} for user arguments. <!-- (6) -->
```

1. **Required.** The full shell command to pipe the prompt to. `-p` enables non-interactive mode, `--dangerously-skip-permissions` lets the agent work autonomously.
2. **Optional.** Each command runs every iteration and its output fills the matching `{{ commands. }}` placeholder.
3. **Optional.** Declares positional argument names. Named flags (`--dir`, `--focus`) work without this — `args` is only needed for positional usage.
4. Everything below the `---` frontmatter is the prompt body. It's re-read from disk every iteration, so you can edit it while the loop runs.
5. Replaced with the command's stdout + stderr. Only commands with a matching placeholder appear in the assembled prompt.
6. Replaced with the `--dir` value from the CLI. Missing args resolve to an empty string.

### Frontmatter fields

| Field | Type | Required | Description |
|---|---|---|---|
| `agent` | string | yes | Full agent command (piped via stdin). See [agent configuration](agents.md) for supported agents. |
| `commands` | list | no | Commands to run each iteration |
| `args` | list | no | User argument names. Letters, digits, hyphens, and underscores only. |
| `credit` | bool | no | Append co-author trailer instruction to prompt (default: `true`) |

### Command fields

| Field | Type | Default | Description |
|---|---|---|---|
| `name` | string | (required) | Identifier for `{{ commands. }}`. Letters, digits, hyphens, and underscores only. |
| `run` | string | (required) | Shell command to execute (supports `{{ args. }}` placeholders). Commands starting with `./` run from the ralph directory; others run from the project root. |
| `timeout` | number | `60` | Max seconds before the command is killed |

No shell features in commands

Commands are run directly, not through a shell — pipes (`|`), redirections (`>`), and chaining (`&&`) won't work in the `run` field. Use a script instead: `run: ./my-script.sh` (scripts starting with `./` run from the ralph directory).

## Placeholders

### Command placeholders

```markdown
{{ commands.tests }}              # Replaced with test command output
{{ commands.git-log }}            # Replaced with git-log command output
```

- Output includes stdout + stderr regardless of exit code
- Only commands referenced by a placeholder appear in the prompt — unreferenced commands still run but their output is excluded
- Unmatched placeholders resolve to empty string
- Must be `commands` (plural)

For details on placeholder resolution, see [How it Works](how-it-works.md#3-resolve-placeholders-with-command-output).

### User argument placeholders

```markdown
{{ args.dir }}                   # Replaced with --dir value from CLI
{{ args.focus }}                 # Replaced with --focus value from CLI
```

- Pass via `ralph run my-ralph --dir ./src --focus "perf"` or `--dir=./src` (named flags)
- Or positionally: `ralph run my-ralph ./src "perf"` (requires `args:` in frontmatter)
- Mixed: `ralph run my-ralph --focus "perf" ./src` — positional args skip names already provided via flags
- `--` ends flag parsing: `ralph run my-ralph -- --verbose ./src` treats `--verbose` as a positional value
- Missing args resolve to empty string
- See [CLI reference → User arguments](cli.md#user-arguments) for full details on flag and positional parsing

### ralph placeholders

```markdown
{{ ralph.name }}               # ralph directory name (e.g. "my-ralph")
{{ ralph.iteration }}          # Current iteration number (1-based)
{{ ralph.max_iterations }}     # Total iterations if -n was set, empty otherwise
```

- Automatically available — no frontmatter configuration needed
- Useful for progress tracking, naming logs, or adjusting behavior near the end of a run
- See [How it Works](how-it-works.md) for more on the loop lifecycle

## The loop

Each iteration:

1. Re-read `RALPH.md` from disk
2. Run all commands in order, capture output
3. Resolve `{{ commands.* }}`, `{{ args.* }}`, and `{{ ralph.* }}` placeholders
4. Pipe assembled prompt to agent via stdin
5. Wait for agent to exit
6. Repeat

### Stopping the loop

| Action | What happens |
|---|---|
| `Ctrl+C` (once) | Finishes the current iteration gracefully, then stops |
| `Ctrl+C` (twice) | Force-kills the agent process and exits immediately |
| `p` | Toggle live peek of the agent's stdout (on by default in an interactive terminal — press to silence, press again to resume) |
| `-n` limit reached | Stops after the specified number of iterations |
| `--stop-on-error` | Stops if agent exits non-zero or times out |

## Live editing

- The prompt body is re-read from disk every iteration — edit the prompt while the loop runs (frontmatter is parsed once at startup)
- HTML comments (``) are stripped from the prompt — use them for notes to yourself

## Common patterns

### Minimal ralph

```markdown
---
agent: claude -p --dangerously-skip-permissions
---

Read TODO.md and implement the next task. Commit when done.
```

### Self-healing with test feedback

```markdown
---
agent: claude -p --dangerously-skip-permissions
commands:
  - name: tests
    run: uv run pytest -x
---

{{ commands.tests }}

Fix failing tests before starting new work.
Read TODO.md and implement the next task.
```

### Parameterized ralph

```markdown
---
agent: claude -p --dangerously-skip-permissions
args: [dir, focus]
---

Research the codebase at {{ args.dir }}.
Focus area: {{ args.focus }}.
```

```bash
ralph run research --dir ./api --focus "error handling"
```

### Debug a single iteration

```bash
ralph run my-ralph -n 1 --log-dir ralph_logs
cat ralph_logs/001_*.log
```

### Run on a branch

```bash
git checkout -b feature && ralph run my-ralph
```

More patterns and real-world examples → [Cookbook](cookbook.md)

## Next steps

- [Getting Started](getting-started.md) — set up your first ralph end-to-end
- [How it Works](how-it-works.md) — what happens inside each iteration
- [Cookbook](cookbook.md) — copy-pasteable ralphs for common tasks
- [Troubleshooting](troubleshooting.md) — common issues and how to fix them

---

# CLI Reference

## `ralph`

With no subcommand, prints the banner and help text.

```bash
ralph            # Show banner and help
ralph --version  # Show version
ralph --help     # Show help
```

| Option | Short | Description |
|---|---|---|
| `--version` | `-V` | Show version and exit |
| `--install-completion` | | Install tab completion for your current shell |
| `--show-completion` | | Print the completion script (for manual setup) |
| `--help` | | Show help and exit |

### Shell completion

```bash
ralph --install-completion bash   # or zsh, fish
```

Restart your shell after installing. Use `--show-completion` to print the script for manual setup.

---

## `ralph run`

Start the [autonomous coding loop](how-it-works.md).

```bash
ralph run my-ralph                         # Run forever (Ctrl+C to stop)
ralph run my-ralph -n 5                    # Run 5 iterations
ralph run my-ralph --stop-on-error         # Stop if agent exits non-zero or times out
ralph run my-ralph --delay 10              # Wait 10s between iterations
ralph run my-ralph --timeout 300           # Kill agent after 5 minutes per iteration
ralph run my-ralph --log-dir ralph_logs    # Save output to log files
ralph run my-ralph --dir ./src             # Pass user args to the ralph
```

| Argument / Option | Short | Default | Description |
|---|---|---|---|
| `PATH` | | (required) | Path to a ralph directory containing `RALPH.md`, a direct path to a `RALPH.md` file, or the name of an installed ralph in `.agents/ralphs/` |
| `-n` | | unlimited | Max number of iterations |
| `--stop-on-error` | `-s` | off | Stop loop if agent exits non-zero or times out |
| `--delay` | `-d` | `0` | Seconds to wait between iterations |
| `--timeout` | `-t` | none | Max seconds per iteration |
| `--log-dir` | `-l` | none | Directory for iteration log files |

### User arguments

User arguments are passed as named flags after the ralph path. Use `{{ args. }}` [placeholders](how-it-works.md#3-resolve-placeholders-with-command-output) in your RALPH.md to reference them.

Named flags (`--name value`) work without any frontmatter declaration. The `args` field is only required when you want to pass **positional** arguments — it tells ralphify which names to map them to:

```markdown
---
agent: claude -p --dangerously-skip-permissions
args: [dir, focus]
---

Research the codebase at {{ args.dir }}.
Focus area: {{ args.focus }}
```

```bash
# Named flags (work with or without args declared in frontmatter)
ralph run research --dir ./my-project --focus "performance"

# Equals syntax works too
ralph run research --dir=./my-project --focus="performance"

# Positional args (requires args: [dir, focus] in frontmatter)
ralph run research ./my-project "performance"

# Mixed — positional args skip names already provided via flags
ralph run research --focus "performance" ./my-project
# dir="./my-project", focus="performance"

# -- ends flag parsing — everything after is positional
ralph run research -- --verbose ./src
# dir="--verbose", focus="./src"
```

Use `--` when a positional value starts with `--` and would otherwise be parsed as a flag.

Missing args resolve to an empty string.

### Stopping the loop

Press `Ctrl+C` to stop the loop. Ralphify uses two-stage signal handling:

| Action | Behavior |
|---|---|
| `Ctrl+C` (first) | Finishes the current iteration gracefully, then stops the loop |
| `Ctrl+C` (second) | Force-kills the agent process and exits immediately |

The first press lets the agent complete its current work (e.g. finish a commit). If you don't want to wait, press `Ctrl+C` again to terminate immediately.

The loop also stops automatically when:

- All `-n` iterations have completed
- `--stop-on-error` is set and the agent exits non-zero or times out

### Peeking at live agent output

When you run `ralph run` in an interactive terminal, the agent's stdout and stderr stream live to the console by default. Press `p` to silence the stream (useful for quieter loops) and press `p` again to resume it. The default is off whenever the output is not a real terminal (piped, redirected, or captured in CI), so `ralph run ... | cat` is unaffected.

Live streaming works with line-buffered agents such as Claude Code, OpenAI Codex, Aider, and any other process that writes one line at a time. When `--log-dir` is set, output is captured to the log file and also echoed after each iteration completes; live peek still works the same way in that mode.

---

## `ralph scaffold`

Scaffold a new ralph with a ready-to-customize template.

```bash
ralph scaffold my-task      # Creates my-task/RALPH.md with a generic template
ralph scaffold              # Creates RALPH.md in the current directory
```

| Argument | Default | Description |
|---|---|---|
| `[NAME]` | none | Directory name. If omitted, creates RALPH.md in the current directory |

The generated template includes an example command (`git-log`), an example arg (`focus`), and a prompt body with placeholders for both. Edit it, then run [`ralph run`](#ralph-run). See [Getting Started](getting-started.md) for a full walkthrough.

Errors if `RALPH.md` already exists at the target location.

---

## RALPH.md format

The `RALPH.md` file is the single configuration and prompt file for a ralph. It uses YAML frontmatter for settings and the body for the prompt text.

```markdown
---
agent: claude -p --dangerously-skip-permissions
commands:
  - name: tests
    run: uv run pytest -x
  - name: git-log
    run: git log --oneline -10
args: [dir, focus]
---

# Prompt body

{{ commands.tests }}

{{ commands.git-log }}

Your instructions here. Reference args with {{ args.dir }}.
```

### Frontmatter fields

| Field | Type | Required | Description |
|---|---|---|---|
| `agent` | string | yes | The full agent command to pipe the prompt to |
| `commands` | list | no | Commands to run each iteration (each has `name` and `run`) |
| `args` | list of strings | no | Declared argument names for user arguments. Letters, digits, hyphens, and underscores only. |
| `credit` | bool | no | Append co-author trailer instruction to prompt (default: `true`) |

### Commands

Each command has these fields:

| Field | Type | Default | Description |
|---|---|---|---|
| `name` | string | (required) | Identifier used in `{{ commands. }}` placeholders. Letters, digits, hyphens, and underscores only. |
| `run` | string | (required) | Shell command to execute each iteration (supports `{{ args. }}` placeholders). Commands starting with `./` run from the ralph directory; others run from the project root. |
| `timeout` | number | `60` | Max seconds before the command is killed |

Commands run in order. Output (stdout + stderr) is captured regardless of exit code. Commands are parsed with `shlex.split()` — no shell features (pipes, redirections, `&&`). For shell features, point the `run` field at a script. See the [Cookbook](cookbook.md) for real-world command patterns.

If a command exceeds its timeout, the process is killed and the captured output up to that point is used.

### Placeholders

| Syntax | Resolves to |
|---|---|
| `{{ commands. }}` | Output of the named command |
| `{{ args. }}` | Value of the named user argument |
| `{{ ralph.name }}` | ralph directory name (e.g. `my-ralph`) |
| `{{ ralph.iteration }}` | Current iteration number (1-based) |
| `{{ ralph.max_iterations }}` | Total iterations if `-n` was set, empty otherwise |

`ralph.*` placeholders are automatically available — no frontmatter configuration needed.

Unmatched placeholders resolve to an empty string.

---

## Next steps

- [How it Works](how-it-works.md) — what happens inside each iteration
- [Quick Reference](quick-reference.md) — condensed cheat sheet for daily use
- [Python API](api.md) — run loops programmatically instead of via the CLI

---

# Python API

TL;DR

`run_loop(config, state)` runs the same loop as `ralph run` but from Python. Build a `RunConfig` with your agent, commands, and options. Pass a `RunState` to inspect progress and control the loop (pause, resume, stop). Add an `EventEmitter` to react to events. Use `RunManager` for concurrent runs.

All public API is available from the top-level `ralphify` package. The API runs the same [core loop](how-it-works.md) as the [CLI](cli.md), so everything you can do with `ralph run` you can do from Python.

## Quick start

```python
from pathlib import Path
from ralphify import run_loop, RunConfig, RunState

config = RunConfig(
    agent="claude -p --dangerously-skip-permissions",
    ralph_dir=Path("my-ralph"),
    ralph_file=Path("my-ralph/RALPH.md"),
    commands=[],
    max_iterations=3,
)
state = RunState(run_id="my-run")
run_loop(config, state)
```

This runs the same loop as [`ralph run my-ralph -n 3`](cli.md#ralph-run). When the loop finishes, `state` contains the results.

---

## `run_loop(config, state, emitter=None)`

The main loop. Reads [RALPH.md](quick-reference.md), runs commands, assembles prompts, pipes them to the agent, and repeats. **Blocks until the loop finishes.**

| Parameter | Type | Description |
|---|---|---|
| `config` | `RunConfig` | All settings for the run |
| `state` | `RunState` | Observable state — counters, status, control methods |
| `emitter` | `EventEmitter | None` | Event listener. `None` uses `NullEmitter` (silent) |

---

## `RunConfig`

All settings for a single run. Fields match the [CLI options](cli.md#ralph-run).

```python
from pathlib import Path
from ralphify import RunConfig, Command

config = RunConfig(
    agent="claude -p --dangerously-skip-permissions",
    ralph_dir=Path("my-ralph"),
    ralph_file=Path("my-ralph/RALPH.md"),
    commands=[
        Command(name="tests", run="uv run pytest -x"),
        Command(name="lint", run="uv run ruff check ."),
    ],
    args={"dir": "./src", "focus": "performance"},
    max_iterations=10,
    delay=2.0,
    timeout=300,
    stop_on_error=True,
    log_dir=Path("ralph_logs"),
    project_root=Path("."),
)
```

| Field | Type | Default | Description |
|---|---|---|---|
| `agent` | `str` | -- | Full agent command string |
| `ralph_dir` | `Path` | -- | Path to the ralph directory |
| `ralph_file` | `Path` | -- | Path to the RALPH.md file |
| `commands` | `list[Command]` | `[]` | Commands to run each iteration |
| `args` | `dict[str, str]` | `{}` | User argument values |
| `max_iterations` | `int | None` | `None` | Max iterations (`None` = unlimited) |
| `delay` | `float` | `0` | Seconds to wait between iterations |
| `timeout` | `float | None` | `None` | Max seconds per iteration |
| `stop_on_error` | `bool` | `False` | Stop loop if agent exits non-zero or times out |
| `log_dir` | `Path | None` | `None` | Directory for iteration log files |
| `credit` | `bool` | `True` | Append co-author trailer instruction to prompt |
| `project_root` | `Path` | `Path(".")` | Project root directory |

`RunConfig` is mutable — you can change fields mid-run, and the loop picks up changes at the next iteration boundary.

---

## `Command`

A command that runs each iteration.

```python
from ralphify import Command

cmd = Command(name="tests", run="uv run pytest -x")
cmd_slow = Command(name="integration", run="uv run pytest tests/integration", timeout=300)
```

| Field | Type | Default | Description |
|---|---|---|---|
| `name` | `str` | (required) | Identifier used in [`{{ commands. }}`](how-it-works.md#3-resolve-placeholders-with-command-output) placeholders. Letters, digits, hyphens, and underscores only. |
| `run` | `str` | (required) | Shell command to execute |
| `timeout` | `float` | `60` | Max seconds before the command is killed |

---

## `RunState`

Observable state for a running loop. Thread-safe control methods let you stop, pause, and resume from another thread.

```python
state = RunState(run_id="my-run")
run_loop(config, state)

print(state.status)      # RunStatus.COMPLETED
print(state.completed)   # 4
print(state.failed)      # 1
print(state.timed_out_count)  # 0  (subset of failed)
print(state.total)       # 5  (completed + failed)
print(state.iteration)   # 5  (current iteration number)
print(state.started_at)  # datetime or None
```

| Property | Type | Description |
|---|---|---|
| `run_id` | `str` | Unique identifier for this run |
| `status` | `RunStatus` | Current lifecycle status |
| `iteration` | `int` | Current iteration number (starts at 0) |
| `completed` | `int` | Number of successful iterations |
| `failed` | `int` | Number of failed iterations (includes timed out) |
| `timed_out_count` | `int` | Number of timed-out iterations (subset of `failed`) |
| `total` | `int` | `completed + failed` |
| `started_at` | `datetime | None` | When the run started |
| `paused` | `bool` | Whether the run is currently paused |
| `stop_requested` | `bool` | Whether a stop has been requested |

Counter invariant

`timed_out_count` is a **subset** of `failed`, not an independent category. A timed-out iteration increments both `timed_out_count` and `failed`. Therefore `completed + failed == total`.

### Control methods

Thread-safe methods for controlling the loop from another thread:

```python
state.request_stop()      # Stop after current iteration
state.request_pause()     # Pause between iterations
state.request_resume()    # Resume a paused loop
```

---

## `RunStatus`

Lifecycle status of a run.

| Status | Value | Description |
|---|---|---|
| `PENDING` | `"pending"` | Created but not yet started |
| `RUNNING` | `"running"` | Loop is executing iterations |
| `PAUSED` | `"paused"` | Paused between iterations, waiting for resume |
| `STOPPED` | `"stopped"` | Stopped by user via `request_stop()` |
| `COMPLETED` | `"completed"` | Reached `max_iterations` or finished naturally |
| `FAILED` | `"failed"` | Stopped by `--stop-on-error` after a failed/timed-out iteration, or crashed with an unhandled exception |

---

## `StopReason`

A `Literal` type indicating why a run stopped. Used in the `RUN_STOPPED` event's `reason` field.

| Value | Description |
|---|---|
| `"completed"` | Reached `max_iterations` or finished naturally |
| `"error"` | Crashed with an unhandled exception |
| `"user_requested"` | Stopped via `request_stop()` or `Ctrl+C` |

```python
from ralphify import StopReason
```

---

## Event system

The loop emits structured events at each step. Implement the `EventEmitter` protocol (a single `emit(event)` method) to listen.

### Custom emitter

```python
from pathlib import Path
from ralphify import Event, EventType, RunConfig, RunState, run_loop

class MyEmitter:
    def emit(self, event: Event) -> None:
        if event.type == EventType.ITERATION_COMPLETED:
            print(f"Iteration {event.data['iteration']} done in {event.data['duration_formatted']}")

config = RunConfig(
    agent="claude -p --dangerously-skip-permissions",
    ralph_dir=Path("my-ralph"),
    ralph_file=Path("my-ralph/RALPH.md"),
    commands=[],
    max_iterations=3,
)
state = RunState(run_id="observed-run")
run_loop(config, state, emitter=MyEmitter())
```

### `Event`

Each event carries:

| Field | Type | Description |
|---|---|---|
| `type` | `EventType` | What happened |
| `run_id` | `str` | Which run produced this event |
| `data` | `dict` | Event-specific payload |
| `timestamp` | `datetime` | UTC timestamp |

Use `event.to_dict()` to serialize to a JSON-compatible dict.

### `EventType` reference

#### Run lifecycle

| Event | Data fields |
|---|---|
| `RUN_STARTED` | `ralph_name`, `commands` (int count), `max_iterations`, `timeout`, `delay` |
| `RUN_STOPPED` | `reason` (`StopReason`), `total`, `completed`, `failed`, `timed_out_count` |
| `RUN_PAUSED` | -- |
| `RUN_RESUMED` | -- |

#### Iteration lifecycle

| Event | Data fields |
|---|---|
| `ITERATION_STARTED` | `iteration` |
| `ITERATION_COMPLETED` | `iteration`, `returncode`, `duration`, `duration_formatted`, `detail`, `log_file`, `result_text` |
| `ITERATION_FAILED` | same as `ITERATION_COMPLETED` |
| `ITERATION_TIMED_OUT` | same as `ITERATION_COMPLETED` (`returncode` is `None`) |

#### Commands

| Event | Data fields |
|---|---|
| `COMMANDS_STARTED` | `iteration`, `count` |
| `COMMANDS_COMPLETED` | `iteration`, `count` |

#### Prompt assembly

| Event | Data fields |
|---|---|
| `PROMPT_ASSEMBLED` | `iteration`, `prompt_length` |

#### Other

| Event | Data fields |
|---|---|
| `AGENT_ACTIVITY` | `iteration`, `raw` (dict — one stream-json line from the agent) |
| `LOG_MESSAGE` | `message`, `level` (`"info"` / `"error"`), `traceback` (optional) |

### Built-in emitters

| Emitter | Description |
|---|---|
| `NullEmitter` | Discards all events silently. Default when no emitter is passed. |
| `QueueEmitter` | Pushes events into a `queue.Queue` for async consumption. |
| `FanoutEmitter` | Broadcasts each event to multiple emitters. |
| `BoundEmitter` | Wraps any emitter with a fixed `run_id` so you don't have to construct `Event` objects manually. Has `log_info(message)` and `log_error(message, traceback=...)` convenience methods. |

```python
from ralphify import QueueEmitter, FanoutEmitter, BoundEmitter

# Consume events from a queue
q_emitter = QueueEmitter()
run_loop(config, state, emitter=q_emitter)
while not q_emitter.queue.empty():
    event = q_emitter.queue.get()
    print(event.to_dict())

# Broadcast to multiple listeners
fanout = FanoutEmitter([q_emitter, MyEmitter()])
run_loop(config, state, emitter=fanout)

# Emit events without constructing Event objects
bound = BoundEmitter(q_emitter, run_id="my-run")
bound(EventType.ITERATION_STARTED, {"iteration": 1})
bound.log_info("Starting phase two")
bound.log_error("Something failed", traceback=tb)
```

---

## Concurrent runs with `RunManager`

`RunManager` is a thread-safe registry for launching and controlling multiple ralph loops concurrently. Each run gets its own daemon thread and event queue.

```python
from pathlib import Path
from ralphify import RunManager, RunConfig, Command

manager = RunManager()

docs_config = RunConfig(
    agent="claude -p --dangerously-skip-permissions",
    ralph_dir=Path("docs-ralph"),
    ralph_file=Path("docs-ralph/RALPH.md"),
    commands=[Command(name="build", run="mkdocs build --strict")],
    max_iterations=5,
)
tests_config = RunConfig(
    agent="claude -p --dangerously-skip-permissions",
    ralph_dir=Path("tests-ralph"),
    ralph_file=Path("tests-ralph/RALPH.md"),
    commands=[Command(name="tests", run="uv run pytest -x")],
    max_iterations=3,
)

docs_run = manager.create_run(docs_config)
tests_run = manager.create_run(tests_config)

manager.start_run(docs_run.state.run_id)
manager.start_run(tests_run.state.run_id)

# Check progress
for run in manager.list_runs():
    print(f"{run.state.run_id}: {run.state.status.value} — {run.state.completed} done")

# Control a run
manager.pause_run(docs_run.state.run_id)
manager.resume_run(docs_run.state.run_id)
manager.stop_run(docs_run.state.run_id)
```

### `ManagedRun`

Each run created by `RunManager` is wrapped in a `ManagedRun` — a bundle of config, state, and event queue.

| Field | Type | Description |
|---|---|---|
| `config` | `RunConfig` | The run's configuration |
| `state` | `RunState` | Observable state — inspect progress or call control methods |
| `emitter` | `QueueEmitter` | Event queue — drain `emitter.queue` to consume events |

#### Adding custom listeners

Register additional emitters to receive events from a run. Add listeners **before** calling `start_run()`:

```python
from ralphify import RunManager, RunConfig, ManagedRun

class SlackNotifier:
    def emit(self, event):
        if event.type == EventType.RUN_STOPPED:
            notify_slack(f"Run {event.run_id} finished")

manager = RunManager()
run = manager.create_run(config)
run.add_listener(SlackNotifier())  # receives all events alongside the queue
manager.start_run(run.state.run_id)
```

When extra listeners are registered, events are broadcast to both the built-in queue and all custom listeners via a `FanoutEmitter`.

### `RunManager` methods

| Method | Description |
|---|---|
| `create_run(config)` | Create a `ManagedRun` from a `RunConfig`. Does not start it. |
| `start_run(run_id)` | Start the run in a daemon thread. |
| `stop_run(run_id)` | Signal the run to stop after the current iteration. |
| `pause_run(run_id)` | Pause the run between iterations. |
| `resume_run(run_id)` | Resume a paused run. |
| `list_runs()` | Return a snapshot of all registered runs. |
| `get_run(run_id)` | Look up a run by ID. |

---

## Next steps

- [**How the loop works**](how-it-works.md) — understand the iteration cycle that `run_loop` executes
- [**Cookbook**](cookbook.md) — real-world ralph examples you can adapt for your own projects

---

# Troubleshooting

Quick checklist

1. Run `ralph run my-ralph -n 1` — it validates your setup and shows clear errors
2. Test the agent outside ralphify: `echo "Say hello" | claude -p`
3. Use `--log-dir ralph_logs` to capture output for debugging
4. Commands don't support shell features (pipes, `&&`) — use a wrapper script instead

Common issues and how to fix them. If your problem isn't listed here, run [`ralph run`](cli.md#ralph-run) with `-n 1` — it validates your setup and shows clear errors.

## Setup issues

### "is not a directory, RALPH.md file, or installed ralph"

The path you passed to [`ralph run`](cli.md#ralph-run) doesn't resolve to a valid ralph. The command accepts a **directory** containing `RALPH.md`, a **direct path** to a `RALPH.md` file, or the **name of an installed ralph** in `.agents/ralphs/`:

```bash
ralph run my-ralph              # directory containing RALPH.md
ralph run my-ralph/RALPH.md     # direct path to the file
ralph run my-ralph              # installed ralph in .agents/ralphs/my-ralph/
```

If you're getting this error, check that the path exists and points to the right place:

```bash
ls my-ralph/RALPH.md                       # local ralph
ls .agents/ralphs/my-ralph/RALPH.md        # installed ralph
```

### "Missing or empty 'agent' field in RALPH.md frontmatter"

Your `RALPH.md` frontmatter is missing the `agent` field or it's an empty string. Add it:

```markdown
---
agent: claude -p --dangerously-skip-permissions
---
```

### "Agent command 'claude' not found on PATH"

The agent CLI isn't installed or isn't in your shell's PATH. Verify by running `claude --version` directly. If it's installed but not found, check your PATH. See [supported agents](agents.md) for setup instructions.

### "RALPH.md already exists"

You ran [`ralph scaffold`](cli.md#ralph-scaffold) in a directory that already contains a `RALPH.md`. Either use a different directory name or edit the existing file:

```bash
# ✗ Fails — RALPH.md already exists in my-ralph/
ralph scaffold my-ralph

# ✓ Option A — use a different name
ralph scaffold my-other-ralph

# ✓ Option B — edit the existing file directly
```

## Loop issues

### Agent produces no output or seems to hang

Try running the [agent command](agents.md) directly to see if it works outside of ralphify:

```bash
echo "Say hello" | claude -p
```

If it hangs there too, the issue is with the agent CLI, not ralphify. If it works standalone but hangs via ralphify, try adding `--timeout` to kill stalled iterations:

```bash
ralph run my-ralph --timeout 300
```

### Agent exits non-zero every iteration

Check the agent's output to understand why. Use `--log-dir` to capture output:

```bash
ralph run my-ralph -n 1 --log-dir ralph_logs
cat ralph_logs/001_*.log
```

Common causes:

- The agent CLI requires authentication that hasn't been set up
- The prompt asks the agent to run a command that fails
- The agent's context window is being exceeded by a very large prompt

If you want the loop to stop on errors instead of continuing, use `--stop-on-error`:

```bash
ralph run my-ralph --stop-on-error --log-dir ralph_logs
```

### Agent runs but doesn't commit

Ralphify doesn't commit for the agent — committing is the agent's responsibility. Make sure your prompt includes explicit commit instructions:

```markdown
## Process
- Run tests before committing
- Commit with a descriptive message like `feat: add X`
```

Also ensure the agent has permission to run git commands. With Claude Code, the `--dangerously-skip-permissions` flag handles this.

### Loop runs too fast / agent not doing anything useful

If iterations finish in seconds with no meaningful work, the agent may be exiting without taking action. Check the logs with [`--log-dir`](cli.md#ralph-run):

```bash
ralph run my-ralph -n 1 --log-dir ralph_logs
cat ralph_logs/001_*.log
```

Common causes:

- The prompt is too vague — tell the agent it's in a loop with no memory between iterations, and give it a specific task
- There's no concrete task source — point the prompt at something like `TODO.md`, `PLAN.md`, or failing tests
- The agent can't find what it's supposed to work on

## Frontmatter issues

### "Invalid YAML in frontmatter"

Your YAML frontmatter has a syntax error — usually a missing colon, bad indentation, or an unquoted special character. The error message includes the YAML parser's details:

```text
Invalid YAML in frontmatter: while parsing a block mapping ...
```

Common fixes:

```yaml
# ✗ Wrong — missing colon, bad indentation
agent claude -p
commands:
- name: tests
run: uv run pytest

# ✓ Correct
agent: claude -p
commands:
  - name: tests
    run: uv run pytest
```

If your value contains special YAML characters (`:`, `#`, `{`, `[`), wrap it in quotes:

```yaml
agent: "claude -p --dangerously-skip-permissions"
```

### "Frontmatter must be a YAML mapping"

The frontmatter parsed as valid YAML but isn't a key-value mapping. This usually happens when the frontmatter is a plain string or a list instead of a mapping:

```yaml
# ✗ Wrong — this is a plain string, not a mapping
---
claude -p --dangerously-skip-permissions
---

# ✓ Correct — key: value pairs
---
agent: claude -p --dangerously-skip-permissions
---
```

### "Each command must have 'name' and 'run' fields"

A command entry in `commands` is missing a required key. Every command needs both `name` and `run`:

```yaml
# ✗ Wrong — missing 'run'
commands:
  - name: tests

# ✗ Wrong — missing 'name'
commands:
  - run: uv run pytest -x

# ✓ Correct
commands:
  - name: tests
    run: uv run pytest -x
```

The related error **"Command '...' must be a non-empty string"** means a `name` or `run` value is empty or not a string.

### "Malformed 'agent' field in RALPH.md frontmatter"

The `agent` value couldn't be parsed as a shell command — usually an unmatched quote:

```yaml
# ✗ Wrong — unclosed quote
agent: claude -p "dangerously-skip-permissions

# ✓ Correct
agent: claude -p --dangerously-skip-permissions
```

### "'credit' must be true or false"

The `credit` field only accepts boolean values. YAML bare words `true`/`false` work, but quoted strings don't:

```yaml
# ✗ Wrong
credit: "yes"
credit: 0

# ✓ Correct
credit: false
credit: true
```

### "Command name contains invalid characters" / "Arg name contains invalid characters"

Command names and arg names may only contain letters, digits, hyphens, and underscores (`a-z`, `A-Z`, `0-9`, `-`, `_`). Names with dots, spaces, or special characters are rejected because they can't be used in [`{{ commands. }}` or `{{ args. }}` placeholders](how-it-works.md#3-resolve-placeholders-with-command-output).

```yaml
# ✗ Wrong — dots and spaces aren't allowed
args: [my.focus, test subject]
commands:
  - name: my.tests
    run: uv run pytest -x
  - name: test suite
    run: uv run pytest -x

# ✓ Correct — use hyphens or underscores
args: [my-focus, test_subject]
commands:
  - name: my-tests
    run: uv run pytest -x
  - name: test_suite
    run: uv run pytest -x
```

### "Duplicate arg name" / "Duplicate command name"

Arg names and command names must be unique within a single `RALPH.md`. Duplicates are rejected at startup:

```yaml
# ✗ Wrong — "tests" appears twice
commands:
  - name: tests
    run: uv run pytest -x
  - name: tests
    run: uv run pytest tests/integration

# ✓ Correct — use distinct names
commands:
  - name: unit-tests
    run: uv run pytest -x
  - name: integration-tests
    run: uv run pytest tests/integration
```

The same applies to `args` — each name must appear only once.

### "'commands' must be a list" or "'args' must be a list of strings"

YAML scalars and lists look similar. A common mistake is writing a plain string where a list is expected:

```yaml
# ✗ Wrong — this is a string, not a list
args: focus
commands: uv run pytest -x

# ✓ Correct — use list syntax
args: [focus]
commands:
  - name: tests
    run: uv run pytest -x
```

`args` must be a list of strings (`[focus]` or `- focus`). `commands` must be a list of `{name, run}` mappings.

### "Command '...' has invalid timeout"

The `timeout` field on a command must be a positive number (in seconds). Strings, zero, and negative values are rejected:

```yaml
# ✗ Wrong
commands:
  - name: tests
    run: uv run pytest -x
    timeout: "five minutes"

# ✓ Correct
commands:
  - name: tests
    run: uv run pytest -x
    timeout: 300
```

## Argument issues

### "Positional argument '...' requires args declared in RALPH.md frontmatter"

You passed a bare value (not a `--name value` flag) to [`ralph run`](cli.md#ralph-run), but your RALPH.md doesn't have an `args` field. Either declare the args or use the `--name value` syntax:

```bash
# ✗ Fails — no args declared, so positional values aren't allowed
ralph run my-ralph "error handling"

# ✓ Option A — declare args in RALPH.md, then pass positionally
# (add `args: [focus]` to frontmatter)
ralph run my-ralph "error handling"

# ✓ Option B — use flag syntax (works even without args declared)
ralph run my-ralph --focus "error handling"
```

### "Too many positional arguments"

You passed more positional values than the `args` field declares:

```bash
# RALPH.md has: args: [focus]
# ✗ Fails — only 1 arg declared, but 2 values given
ralph run my-ralph "error handling" "api module"

# ✓ Correct — one positional value per declared arg
ralph run my-ralph "error handling"
```

### "Flag '--...' requires a value"

A `--name` flag was passed without a value:

```bash
# ✗ Fails — --focus has no value
ralph run my-ralph --focus

# ✓ Correct
ralph run my-ralph --focus "error handling"
```

## Command issues

### "Command '...' has invalid syntax"

A command's `run` string has malformed shell syntax — usually an unmatched quote. The error message tells you which command failed:

```text
Command 'tests' has invalid syntax: 'uv run pytest -x "unclosed'. Check the 'commands' field in your RALPH.md frontmatter.
```

Fix the quoting in the `run` value. If your command needs complex quoting, point it at a script instead — see [Command with pipes or redirections not working](#command-with-pipes-or-redirections-not-working).

### "Command '...' binary not found"

A command in your `commands` field references a binary that isn't installed or isn't on your PATH. The error message tells you which command failed:

```text
Command 'lint' binary not found: 'mypy src/'. Check the 'commands' field in your RALPH.md frontmatter.
```

Verify the binary exists by running it directly:

```bash
mypy --version
```

If it's installed in a virtual environment, prefix the command with `uv run` or the appropriate runner:

```yaml
commands:
  - name: lint
    run: uv run mypy src/
```

### Command with pipes or redirections not working

Commands in the `run` field are parsed with `shlex` and run **directly** — not through a shell. Shell features like pipes (`|`), redirections (`2>&1`), chaining (`&&`), and variable expansion (`$VAR`) silently fail or produce unexpected results. The escape hatch is a script — wrap the shell features inside a `.sh` file and point the command at it.

**Won't work:**

```yaml
commands:
  - name: tests
    run: pytest --tb=line -q 2>&1 | tail -20
```

**Fix:** Point the command at a script instead:

```bash
#!/bin/bash
# scripts/run-tests.sh
pytest --tb=line -q 2>&1 | tail -20
```

```bash
chmod +x scripts/run-tests.sh
```

```yaml
commands:
  - name: tests
    run: scripts/run-tests.sh
```

Commands without a `./` prefix run from the project root, so `scripts/run-tests.sh` resolves to `/scripts/run-tests.sh`. If you want to bundle the script next to your `RALPH.md`, use the `./` prefix instead — see [Working directory](how-it-works.md#2-run-commands-and-capture-output) for details.

### Command always failing

Run the command manually to see if it works:

```bash
uv run pytest -x
```

If the command fails manually, the issue isn't with ralphify — fix the underlying test/lint failures first.

Note that command output is included in the prompt **regardless of exit code**. A failing test command is often exactly what you want — the agent sees the failure and fixes it.

### Command output looks truncated

Each command has a default timeout of **60 seconds** (see [Run commands and capture output](how-it-works.md#2-run-commands-and-capture-output)). If your command takes longer (a large test suite, a slow build), it's killed at the timeout and only the output captured so far is used. The agent sees a notice appended to the output:

```text
[Command 'tests' timed out after 60s — output may be incomplete]
```

**Fix:** Increase the timeout for slow commands:

```yaml
commands:
  - name: tests
    run: uv run pytest -x
    timeout: 300  # 5 minutes
```

You can also speed up the command itself — for example, running a subset of tests or filtering output via a [wrapper script](#command-with-pipes-or-redirections-not-working).

### Command output missing from prompt

If a `{{ commands.my-command }}` placeholder produces nothing in the prompt:

1. Check the command name matches exactly: `{{ commands.my-command }}` requires a command with `name: my-command`
2. Verify the command produces output by running it manually
3. Must be `commands` (plural) — `{{ command.name }}` won't resolve

No output visible during iteration

By default, agent output goes directly to the terminal. If you're using `--log-dir`, output is captured and then replayed — you'll still see it, but only after the iteration completes.

CLI flag validation errors

### "'-n' must be a positive integer"

The `-n` flag sets how many iterations to run. It must be at least 1:

```bash
# ✗ Wrong
ralph run my-ralph -n 0

# ✓ Correct
ralph run my-ralph -n 1
```

### "'--delay' must be non-negative"

The `--delay` flag sets seconds to wait between iterations. It can be zero but not negative:

```bash
# ✗ Wrong
ralph run my-ralph --delay -5

# ✓ Correct
ralph run my-ralph --delay 0
ralph run my-ralph --delay 30
```

### "'--timeout' must be a positive number"

The `--timeout` flag sets the per-iteration time limit in seconds. It must be greater than zero:

```bash
# ✗ Wrong
ralph run my-ralph --timeout 0

# ✓ Correct
ralph run my-ralph --timeout 300
```

## Common questions

### Can I run multiple loops in parallel?

Yes, but they should work on **separate branches** to avoid git conflicts:

```bash
# Terminal 1
git checkout -b feature-a && ralph run feature-a-ralph

# Terminal 2
git checkout -b feature-b && ralph run feature-b-ralph
```

For programmatic control over concurrent runs, use the [Python API's `RunManager`](api.md#concurrent-runs-with-runmanager).

### What files should I commit?

| File / directory | Commit? | Why |
|---|---|---|
| `my-ralph/RALPH.md` | **Yes** | The ralph definition |
| `my-ralph/*.sh` | **Yes** | Helper scripts referenced by commands |
| `ralph_logs/` | **No** | Iteration logs — add to `.gitignore` |

### Can I edit RALPH.md while the loop runs?

Yes. The prompt body (everything below the frontmatter) is re-read every iteration — edit the prompt text and changes take effect on the next cycle. Frontmatter fields (`agent`, `commands`, `args`) are parsed once at startup, so changing those requires restarting the loop.

### How do I disable the co-author credit in commits?

By default, ralphify appends an instruction asking the agent to add `Co-authored-by: Ralphify ` to commit messages. To disable it, set `credit: false` in your RALPH.md frontmatter:

```yaml
---
agent: claude -p --dangerously-skip-permissions
credit: false
---
```

### How do I make a ralph reusable across projects?

Use `args` to parameterize your ralph instead of hardcoding project-specific values:

```markdown
---
agent: claude -p --dangerously-skip-permissions
args: [dir, focus]
commands:
  - name: tests
    run: uv run pytest {{ args.dir }}
---

Focus on {{ args.dir }}. Priority: {{ args.focus }}
```

```bash
ralph run my-ralph --dir ./api --focus "error handling"
ralph run my-ralph --dir ./frontend --focus "accessibility"
```

See [User arguments](cli.md#user-arguments) for more on parameterizing ralphs.

## Getting more help

1. Run `ralph run my-ralph -n 1` to validate your setup — it shows clear errors
2. Use `ralph run my-ralph -n 1 --log-dir ralph_logs` to capture a single iteration for debugging
3. Check the [CLI reference](cli.md) for all available options
4. File an issue at [github.com/computerlovetech/ralphify](https://github.com/computerlovetech/ralphify/issues)

## Next steps

- [How it works](how-it-works.md) — understand the iteration lifecycle to debug more effectively
- [Cookbook](cookbook.md) — working examples you can adapt for your project

---

# Changelog

All notable changes to ralphify are documented here.

## 0.3.0 — 2026-03-24

### Added

- **`ralph add` — install ralphs from GitHub** — share and reuse ralphs across projects. Run `ralph add owner/repo/ralph-name` to fetch a ralph from any GitHub repo and install it locally. Then just `ralph run ralph-name`. Supports installing a single ralph by name, an entire repo as a ralph, or all ralphs in a repo at once. Installed ralphs live in `.ralphify/ralphs/` (gitignored, disposable).
- **Two-stage Ctrl+C** — first Ctrl+C gracefully finishes the current iteration, second Ctrl+C force-stops immediately. Agent subprocesses now run in their own process group for reliable cleanup.
- **Iteration monitor UI** — iteration results are now rendered as markdown using Rich, with a polished run header and cleaner formatting. Thanks to [@malpou](https://github.com/malpou) for contributing this improvement.

### Improved

- **Process group isolation** — agent subprocesses (both streaming and blocking) now run in dedicated process groups, preventing zombie processes on timeout or cancellation.

---

## 0.2.5 — 2026-03-22

### Added

- **ralph placeholders** — ralphs can now access runtime metadata via `{{ ralph.name }}` (ralph directory name), `{{ ralph.iteration }}` (current iteration, 1-based), and `{{ ralph.max_iterations }}` (total iterations if `-n` was set, empty otherwise). No frontmatter configuration needed.

---

## 0.2.4 — 2026-03-22

### Fixed

- **Placeholder cross-contamination between args and commands** — when an arg value contained text like `{{ commands.tests }}`, the sequential resolution would re-process it as a command placeholder, injecting unrelated output. Placeholders are now resolved in a single pass so inserted values are never re-scanned.
- **Helpful error when command binary is not found** — when a `commands` entry references a binary that isn't installed (e.g. `run: mypy src/`), the error now identifies which command failed and points to the `commands` field. Previously this surfaced as a generic crash with no context.
- **Timed-out agent output not echoed when logging enabled** — when using `--log-dir` and the agent timed out, partial output was written to the log file but silently swallowed from the terminal. Both completion and timeout paths now echo consistently.
- **`--` separator not ending flag parsing in user args** — `ralph run my-ralph -- --verbose ./src` now correctly treats `--verbose` as a positional value instead of parsing it as a flag.
- **Command output garbled when stdout lacked trailing newline** — when a command's stdout didn't end with a newline and stderr was non-empty, the two streams were concatenated directly (e.g. `"test passedwarning: dep"`), producing garbled output in log files and `{{ commands.* }}` placeholder values.
- **Indented `---` in YAML block scalars mistaken for closing frontmatter delimiter** — the frontmatter parser used `line.strip()` to detect the closing `---`, which caused indented `---` inside YAML block scalars (e.g. `notes: |` with `  ---` content) to be treated as the end of the frontmatter. Now only `---` at column 0 is recognized as the closing delimiter.
- **Indented opening `---` delimiter accepted inconsistently** — the opening frontmatter delimiter check used `strip()` which accepted leading whitespace (e.g. `  ---`), while the closing delimiter required column 0. Both delimiters now require `---` at column 0 per the YAML frontmatter spec.
- **Arg values with spaces breaking command execution** — when `{{ args.name }}` placeholders were substituted into command `run` strings, values containing spaces (e.g. `"hello world"`) were split into separate tokens by `shlex.split`, causing the wrong command to execute. Arg values are now shell-quoted before substitution so they are always treated as single tokens.
- **Helpful error when command has invalid syntax** — when a command's `run` string has malformed shell syntax (e.g. unmatched quotes), the error now identifies which command failed and points to the `commands` field. Previously this surfaced as a bare `ValueError` like "No closing quotation" with no context.
- **Empty arg values breaking `./` working directory detection** — when an `{{ args.name }}` placeholder resolved to an empty string, the substitution could introduce leading whitespace that prevented the `./` prefix from being detected, causing the command to run from the project root instead of the ralph directory.
- **Windows `.cmd`/`.exe` extension breaking streaming mode detection** — on Windows, `claude` is installed as `claude.cmd` or `claude.exe`. The streaming mode check compared the full filename (including extension) against `"claude"`, so it never matched. Ralphify now compares the stem only, enabling real-time activity tracking on Windows.

### Improved

- **`BoundEmitter` convenience methods** — `log_info(message)` and `log_error(message, traceback=...)` let Python API users emit log events without constructing `Event` objects manually.
- **Extracted `ProcessResult` base class** — `RunResult` and `AgentResult` now share a common base with consistent `success` / `timed_out` semantics, reducing duplication in `_runner.py` and `_agent.py`.
- **Code quality** — extracted CLI validation helpers, renamed `resolver.py` to `_resolver.py` to match private module convention, deduplicated output echoing and timeout/blocking paths in `_agent.py`, extracted `ensure_str` helper for consistent bytes-to-string decoding.

---

## 0.2.3 — 2026-03-21

### Added

- **Co-authored-by credit trailer** — every prompt now includes an instruction telling the agent to add `Co-authored-by: Ralphify ` to commit messages. On by default; opt out with `credit: false` in RALPH.md frontmatter.

### Improved

- **Typed event payloads** — replaced `dict[str, Any]` event data with TypedDict classes throughout the engine and console emitter for stronger type safety.
- **Code quality** — standardized imports, extracted constants, simplified TypedDicts with `NotRequired`.

---

## 0.2.2 — 2026-03-21

### Added

- **`ralph init` command** — scaffold a new ralph with a ready-to-customize template, no AI agent required. Run `ralph init my-task` to create a directory with a `RALPH.md` that includes example commands, args, and placeholders. A faster alternative to the AI-guided `ralph new`.
- **`ralphify-cowork` skill** — a Claude Cowork skill that lets non-technical users set up and run autonomous loops from plain English. Handles installation, ralph creation, running, and tweaking — no coding knowledge needed. Install it in Cowork from `skills/ralphify-cowork/`.

---

## 0.2.1 — 2026-03-21

### Fixed

- **`{{ args.* }}` placeholders now resolved in command `run` strings** — previously, arg placeholders were only resolved in the prompt body. Commands like `run: gh issue view {{ args.issue }}` would fail because `shlex.split` tokenized the raw placeholder into multiple arguments. Args are now resolved before command execution.

---

## 0.2.0 — 2026-03-21

The v2 rewrite. Ralphify is now simpler: a ralph is a directory with a `RALPH.md` file. No more `ralph.toml`, no more `.ralphify/` directory, no more `ralph init`. Everything lives in one file.

### Breaking changes

- **Removed `ralph.toml`** — the agent command is now in the `agent` field of RALPH.md frontmatter. No separate configuration file.
- **Removed `ralph init`** — create a ralph directory with a `RALPH.md` file manually, or use `ralph new`.
- **Removed `.ralphify/` directory** — no more checks, contexts, or named ralphs as separate primitives. Everything is defined in RALPH.md.
- **Removed checks and contexts** — replaced by `commands` in RALPH.md frontmatter. Commands run each iteration and their output is available via `{{ commands. }}` placeholders.
- **`ralph run` requires a path** — `ralph run my-ralph` instead of `ralph run` with optional name. The argument is a path to a directory containing RALPH.md.
- **Placeholder syntax changed** — `{{ contexts. }}` is now `{{ commands. }}`. The `{{ args. }}` syntax is unchanged.
- **User arguments** — pass named flags to your ralph: `ralph run my-ralph --dir ./src`.

### Added

- **`commands` frontmatter field** — define commands that run each iteration directly in RALPH.md. Each command has a `name` and `run` field.
- **Single-file configuration** — the `agent` field, commands, args, and prompt all live in one RALPH.md file.

### Fixed

- **Guard against double-starting a run** — `RunManager` now prevents the same run from being started twice.
- **Eliminate TOCTOU race in RunManager** — `start_run` is now atomic to prevent race conditions in concurrent runs.
- **UTF-8 encoding for all subprocess calls** — prevents encoding errors on systems with non-UTF-8 defaults.
- **Stricter input validation** — reject whitespace-only agent fields, command names, command `run` values, and command strings. Validate negative delay, non-positive `max_iterations`, and timeout values. Validate `commands` field type and reject duplicate command names. Validate command `timeout` field in frontmatter.
- **Clear command placeholders when no commands exist** — matches the existing behavior for args placeholders.
- **Error handling for `os.execvp` in `ralph new`** — graceful error instead of unhandled exception when the agent binary is not found.

### Improved

- **Extensive test coverage** — added unit tests for `_agent.py`, `_events.py`, `ConsoleEmitter`, engine internals, streaming agent execution, command cwd logic, and frontmatter edge cases.
- **Code quality** — extracted magic strings into named constants, consolidated duplicate test helpers, replaced lambda closures with `functools.partial`, and compiled module-level regexes in the resolver.

### How to upgrade from 0.1.x

1. **Move agent config into RALPH.md** — take the `command` and `args` from `ralph.toml` and combine them into the `agent` field in RALPH.md frontmatter.

2. **Convert checks and contexts to commands** — each check/context becomes a command entry:

    ```yaml
    # Before (separate files):
    # .ralphify/checks/tests/CHECK.md with command: uv run pytest -x
    # .ralphify/contexts/git-log/CONTEXT.md with command: git log --oneline -10

    # After (in RALPH.md frontmatter):
    commands:
      - name: tests
        run: uv run pytest -x
      - name: git-log
        run: git log --oneline -10
    ```

3. **Update placeholders** — change `{{ contexts. }}` to `{{ commands. }}`.

4. **Move RALPH.md into a directory** — create a directory for your ralph and put RALPH.md inside it.

5. **Delete old files** — remove `ralph.toml` and the `.ralphify/` directory.

6. **Update CLI usage** — `ralph run` becomes `ralph run `.

---

## 0.1.12 — 2026-03-20

### Changed

- **New tagline** — updated project tagline to "Stop stressing over not having an agent running. Ralph is always running" across CLI, PyPI, and docs.

---

## 0.1.11 — 2026-03-18

### Improved

- **`ralph new` runs without permission prompts** — Claude Code is now launched with `--dangerously-skip-permissions` so the AI-guided setup flow is uninterrupted.
- **Simpler `ralph new` experience** — the skill no longer asks users about checks, contexts, or frontmatter. Just describe what you want to automate in plain English and the agent builds the ralph for you.
- **`ralph new` knows about user arguments** — the skill can now suggest `{{ args.name }}` placeholders when a ralph would benefit from being reusable across projects.

---

## 0.1.10 — 2026-03-18

### Added

- **User arguments for ralphs** — pass `--name value` flags or positional args to `ralph run` and reference them in prompts with `{{ args.name }}` placeholders. Declare positional arg names in frontmatter with `args: [dir, focus]`. Context and check scripts receive user arguments as `RALPH_ARG_` environment variables.
- **Quick reference page** — single-page cheat sheet covering CLI commands, directory structure, frontmatter fields, placeholders, and common patterns.
- **Prompt writing guide** — best practices for writing effective RALPH.md prompts, including tips on scoping, context usage, and user arguments.
- **"How it works" page** — explains the iteration lifecycle so users understand the system model.
- **"When to use" guide** — helps users evaluate whether ralph loops fit their task.
- **Agent comparison table** — side-by-side comparison of supported agents with output behavior notes.
- **Expanded cookbook** — new recipes for Python, TypeScript, Rust, Go, bug fixing, codebase migration, and multi-ralph project setup.

### Fixed

- Malformed `ralph.toml` now shows a helpful error message instead of a raw `KeyError`.

---

## 0.1.9 — 2026-03-16

Tightened the primitive system: global primitives are now opt-in, context placeholders must be named, and primitives are re-discovered every iteration.

### Breaking changes

- **Explicit primitive dependencies required** — global checks and contexts are no longer auto-applied to all ralphs. Each ralph must declare which global primitives it uses in its frontmatter (`checks: [lint, tests]`, `contexts: [git-log]`). Unknown names produce a clear error. ralph-local primitives still auto-apply.
- **Named placeholders only** — the bulk `{{ contexts }}` placeholder and implicit append behavior have been removed. Each context must be referenced by name (`{{ contexts.git-log }}`). Contexts not referenced by a placeholder are excluded from the prompt.
- **Removed `ralph status` command** — setup validation has been moved into `ralph run` startup, so a separate status command is no longer needed.

### Added

- **Live re-discovery** — primitives are re-discovered every iteration, so adding or editing a check, context, or ralph on disk takes effect on the next cycle without restarting the loop.

---

## 0.1.8 — 2026-03-16

Redesigned `ralph new` with AI-guided setup, and added environment variables for scripts.

### Added

- **AI-guided `ralph new`** — `ralph new` now installs a skill into your agent (Claude Code or Codex) and launches an interactive session where the agent guides you through creating a complete ralph — prompt, checks, and contexts — via conversation. Replaces the old `ralph new check`/`ralph new context`/`ralph new ralph` scaffolding subcommands.
- **`RALPH_NAME` environment variable** — context and check scripts now receive the name of the current ralph in `RALPH_NAME`, so scripts can adapt their behavior based on which ralph is running.

---

## 0.1.7 — 2026-03-12

Simplified the CLI, added a spinner during iterations, and removed the experimental web dashboard to focus on the CLI experience.

### Breaking changes

- **Removed `ralph ui` subcommand and web dashboard** — the experimental web dashboard introduced in 0.1.6 has been removed.
- **Removed `ralph ralphs` subcommand** — `ralph new ` is now shorthand for `ralph new ralph `.
- **Removed instructions primitive** — the `instructions` primitive type introduced in 0.1.3 has been removed. Use contexts for injecting reusable rules into prompts instead.
- **Removed ad-hoc prompts (`-p` flag)** — the `--prompt` / `-p` flag on `ralph run` has been removed.

### Added

- **Spinner with elapsed time** — iterations now show a live spinner with elapsed seconds.

### Fixed

- Agent result message is now displayed in CLI output after each iteration.
- Raw Claude Code `stream-json` output no longer leaks to the terminal during iterations.

---

## 0.1.6 — 2026-03-12

Named ralphs, ralph-scoped primitives, and live agent activity streaming.

### Added

- **Named ralphs** — save reusable, task-focused ralphs in `.ralphify/ralphs//RALPH.md` and switch between them with `ralph run `.
- **Live agent activity streaming** — when the agent command is Claude Code, the engine auto-detects it and uses `--output-format stream-json` with `subprocess.Popen` for line-by-line streaming.
- **Contributor docs** — new `docs/contributing/` section with a codebase map.

### Fixed

- `RUN_STOPPED` event now emits exactly once with the correct stop reason.
- Windows compatibility fix for Unicode characters in terminal output.

---

## 0.1.5 — 2026-03-11

Dashboard UI polish (experimental — dashboard removed in 0.1.7).

---

## 0.1.4 — 2026-03-11

Ad-hoc prompts, better discoverability, and expanded cookbook recipes.

### Added

- **Ad-hoc prompts** — `ralph run -p "Fix the login bug"` passes a prompt directly on the command line.
- **Rust and Go cookbook recipes** — complete copy-pasteable setups.

---

## 0.1.3 — 2026-03-10

The primitives release. Checks, contexts, and instructions turn the basic loop into a self-healing feedback system.

### Added

- **Checks** — post-iteration validation scripts in `.ralphify/checks/`.
- **Contexts** — dynamic data injection in `.ralphify/contexts/`.
- **Instructions** — reusable prompt rules in `.ralphify/instructions/`.

---

## 0.1.2 — 2026-03-09

Quality-of-life improvements for the core loop.

### Added

- `ralph status` command
- `--timeout` / `-t` option
- `--log-dir` / `-l` option
- `--version` / `-V` flag
- ASCII art startup banner

---

## 0.1.0 — 2026-03-09

Initial release.

### Added

- `ralph init` — create `ralph.toml` and `RALPH.md` in your project
- `ralph run` — the core autonomous loop: read prompt, pipe to agent, repeat
- Iteration tracking with exit codes and duration
- `--stop-on-error` / `-s`, `-n`, `--delay` / `-d`
- Auto-detection of project type during `ralph init`

---

# Contributing

Ralphify is open source (MIT) and welcomes contributions. This page covers everything you need to set up a development environment, run tests, and submit changes.

For architecture details and codebase orientation, see the [Codebase Map](codebase-map.md).

## Development setup

Clone the repository and install dependencies with [uv](https://docs.astral.sh/uv/):

```bash
git clone https://github.com/computerlovetech/ralphify.git
cd ralphify
uv sync
```

This installs all runtime and dev dependencies (pytest, mkdocs, mkdocs-material) into a local virtual environment.

Verify the setup by running the CLI from source:

```bash
uv run ralph --version
```

## Running tests

The test suite uses pytest with one test file per source module:

```bash
uv run pytest           # Run all tests
uv run pytest -x        # Stop on first failure
uv run pytest -v        # Verbose output
```

Tests use temporary directories and have no external dependencies — no API keys, no network access, no Docker.

When adding a new feature, add tests in the corresponding file. If you're adding a new module, create a matching `test_.py` file.

## Working on documentation

The docs site uses [MkDocs](https://www.mkdocs.org/) with the [Material](https://squidfunk.github.io/mkdocs-material/) theme.

### Local preview

```bash
uv run mkdocs serve
```

This starts a local server at `http://127.0.0.1:8000` with live reload — edits to docs files appear instantly in the browser.

### Build check

```bash
uv run mkdocs build --strict
```

The `--strict` flag treats warnings as errors. The CI pipeline uses this flag, so make sure your changes build cleanly before submitting.

## Working on the website

The deployed site combines a static **landing page** (`website/`) and the **MkDocs docs** (`docs/`). The `docs.yml` GitHub Actions workflow builds both and deploys them together to GitHub Pages.

### Local preview

Use the [justfile](https://github.com/casey/just) for common build tasks:

```bash
just docs-preview         # MkDocs dev server at http://127.0.0.1:8000
just website-build        # Build the full combined site to _site/
just website-preview      # Build + serve at http://127.0.0.1:8080
```

## Submitting changes

1. **Fork and branch** — create a feature branch from `main`:

    ```bash
    git checkout -b my-feature
    ```

2. **Make your changes** — keep commits focused and atomic.

3. **Run tests** — make sure all tests pass:

    ```bash
    uv run pytest
    ```

4. **Check the docs** — if you changed anything in `docs/` or `mkdocs.yml`:

    ```bash
    uv run mkdocs build --strict
    ```

5. **Open a pull request** against `main` with a clear description of what you changed and why.

### Commit messages

The project uses descriptive commit messages that explain the user benefit:

```text
docs: explain X for users who want to Y
feat: add X so users can Y
fix: resolve X that caused Y
```

Look at `git log --oneline` for examples of the style.

## Dependencies

Ralphify is minimal by design:

- **Runtime:** `typer` (CLI framework), `rich` (terminal formatting), and `pyyaml` (YAML frontmatter parsing)
- **Dev:** `pytest`, `mkdocs`, `mkdocs-material`

Think carefully before adding a new dependency. If it can be done with the standard library, prefer that.

## Release process

Releases are published to PyPI automatically when a GitHub release is created:

1. Update the version in `pyproject.toml`
2. Create a GitHub release with a tag matching the version (e.g. `v0.2.0`)
3. The `publish.yml` workflow runs tests, builds the package, verifies the version matches the tag, and publishes to PyPI

Docs deploy automatically to GitHub Pages on every push to `main` that changes files in `docs/` or `mkdocs.yml`.

---

# Codebase Map

Quick orientation guide for anyone working on this codebase — human contributors and AI coding agents alike.

## What this project is

Ralphify is a CLI tool (`ralph`) that runs AI coding agents in autonomous loops. It reads a RALPH.md file from a ralph directory, runs commands, assembles a prompt with the output, pipes it to an agent command via stdin, waits for it to finish, then repeats. Each iteration gets a fresh context window. Progress is tracked through git commits.

The core loop is simple. The complexity lives in **prompt assembly** — running commands and resolving placeholders into the prompt before each iteration.

## Directory structure

```
src/ralphify/           # All source code
├── __init__.py         # Version detection + app entry point
├── cli.py              # CLI commands (run, scaffold) — delegates to engine for the loop
├── engine.py           # Core run loop orchestration with structured event emission
├── manager.py          # Multi-run orchestration (concurrent runs via threads)
├── _resolver.py        # Template placeholder resolution ({{ commands.* }}, {{ args.* }}, {{ ralph.* }})
├── _agent.py           # Run agent subprocesses (streaming + blocking modes, log writing)
├── _run_types.py       # RunConfig, RunState, RunStatus, Command — shared data types
├── _runner.py          # Execute shell commands with timeout and capture output
├── _frontmatter.py     # Parse YAML frontmatter from RALPH.md, marker constants
├── _console_emitter.py # Rich console renderer for run-loop events (ConsoleEmitter)
├── _events.py          # Event types, emitter protocol, and BoundEmitter convenience wrapper
├── _keypress.py        # Cross-platform single-keypress listener (powers the `p` peek toggle)
├── _output.py          # ProcessResult base class, combine stdout+stderr, format durations
└── _brand.py           # Brand color constants shared across CLI and console rendering

tests/                  # Pytest tests — one test file per module
docs/                   # MkDocs site (Material theme) — user-facing documentation
docs/contributing/      # Contributor documentation (this section)
.github/workflows/
├── test.yml            # Run tests on push to main and PRs (Python 3.11–3.13)
├── docs.yml            # Deploy docs to GitHub Pages on push to main
└── publish.yml         # Publish to PyPI on release (with test gate)
```

## Architecture: how the pieces connect

The CLI entry point is `cli.py:run()`, which parses options, reads the ralph directory path, and delegates to `engine.py:run_loop()` for the actual iteration cycle. The engine emits structured events via an `EventEmitter`, making the same loop reusable from both the CLI and any external orchestration layer (such as `manager.py`).

```
ralph run my-ralph
  │
  ├── cli.py:run() — parse options, print banner
  │   ├── Read RALPH.md from the given directory
  │   ├── Parse frontmatter (agent, commands, args)
  │   └── Build RunConfig and call engine.run_loop()
  │
  └── engine.py:run_loop(config, state, emitter)
       └── Loop:
            ├── Re-read RALPH.md from disk
            ├── Run commands → capture output
            ├── Resolve {{ commands.* }}, {{ args.* }}, and {{ ralph.* }} placeholders
            ├── Pipe assembled prompt to agent command via subprocess
            ├── Emit iteration events (started, completed, failed, timed_out)
            ├── Handle pause/resume/stop requests via RunState
            └── Repeat
```

### Placeholder resolution

The resolver (`_resolver.py`) handles three placeholder kinds: `{{ commands. }}`, `{{ args. }}`, and `{{ ralph. }}`. Two functions:

- **`resolve_all()`** — resolves all three placeholder kinds in a **single pass** so that a value inserted by one kind (e.g., an arg value containing `{{ commands.foo }}`) is never re-processed as the other kind. Used by the engine for final prompt assembly. The `ralph.*` placeholders (`ralph.name`, `ralph.iteration`, `ralph.max_iterations`) provide runtime metadata and require no frontmatter configuration.
- **`resolve_args()`** — resolves only `{{ args. }}` placeholders. Used by the engine to expand arg references inside command `run` strings before executing them.

Unmatched placeholders resolve to empty string in both functions.

### Event system

The run loop communicates via structured events (`_events.py`). Each event has a type (`EventType` enum), run ID, typed data payload, and UTC timestamp.

Event data uses TypedDict classes — one per event type — rather than free-form dicts. The key types:

- **`RunStartedData`** / **`RunStoppedData`** — run lifecycle (stop reason is a `StopReason` literal: `"completed"`, `"error"`, `"user_requested"`)
- **`IterationStartedData`** / **`IterationEndedData`** — per-iteration data (return code, duration, log path)
- **`CommandsStartedData`** / **`CommandsCompletedData`** — command execution bookends
- **`PromptAssembledData`** — prompt length after placeholder resolution
- **`AgentActivityData`** — streaming agent output
- **`LogMessageData`** — info/error messages with optional traceback

All payload types are unioned as `EventData`.

Emitter implementations:

- **`EventEmitter`** — protocol that any listener implements (just an `emit(event)` method)
- **`NullEmitter`** — discards events (used in tests)
- **`QueueEmitter`** — pushes events into a `queue.Queue` for async consumption
- **`FanoutEmitter`** — broadcasts events to multiple emitters
- **`BoundEmitter`** — wraps any emitter with a fixed run ID, so callers don't have to pass the ID on every emit. The engine creates one per run and threads it through all internal functions.

The CLI uses a `ConsoleEmitter` (defined in `_console_emitter.py`) that renders events to the terminal with Rich formatting.

### Multi-run management

`manager.py:RunManager` orchestrates concurrent runs:

- Creates runs with unique IDs and wraps them in `ManagedRun` (config + state + emitter + thread)
- Starts each run in a daemon thread via `engine.run_loop()`
- Supports pause/resume/stop per run via `RunState` thread-safe control methods
- Uses `FanoutEmitter` to broadcast events to multiple listeners

## Key files to understand first

1. **`engine.py`** — The core run loop. Uses `RunConfig` and `RunState` (from `_run_types.py`) and `EventEmitter`. This is where iteration logic lives.
2. **`_run_types.py`** — `RunConfig`, `RunState`, `RunStatus`, and `Command`. These are the shared data types used by the engine, CLI, and manager.
3. **`cli.py`** — All CLI commands. Validates frontmatter fields via extracted helpers (`_validate_agent`, `_validate_commands`, `_validate_credit`, `_validate_run_options`, `_validate_declared_args`), builds a `RunConfig`, and delegates to `engine.run_loop()` for the actual loop. Terminal event rendering lives in `_console_emitter.py`.
4. **`_frontmatter.py`** — YAML frontmatter parsing. Extracts `agent`, `commands`, `args` from the RALPH.md file.
5. **`_resolver.py`** — Template placeholder logic. Small file but critical.

## Traps and gotchas

### If you change frontmatter fields...

Frontmatter parsing is in `_frontmatter.py:parse_frontmatter()`, which returns a raw dict. Each field is then validated and coerced by a dedicated helper in `cli.py` — e.g. `_validate_agent()`, `_validate_commands()`, `_validate_credit()`. Adding a new frontmatter field means adding a new validator in `cli.py` and wiring it into `_build_run_config()`.

**Field name constants** (`FIELD_AGENT`, `FIELD_COMMANDS`, `FIELD_ARGS`, `FIELD_CREDIT`, `CMD_FIELD_NAME`, `CMD_FIELD_RUN`, `CMD_FIELD_TIMEOUT`) are centralized in `_frontmatter.py`. Always import these constants instead of hardcoding strings like `"agent"` or `"commands"` — this keeps error messages, validation, and placeholder resolution in sync when fields are renamed.

### If you add a new CLI command...

Add it in `cli.py`. The CLI uses Typer. Update `docs/cli.md` to document the new command.

### If you change the event system...

Events are defined in `_events.py:EventType`, with a corresponding TypedDict payload class for each type. Adding a new event type requires a new `EventType` member, a new TypedDict payload class, adding it to the `EventData` union, and handling it in `ConsoleEmitter` (`_console_emitter.py`).

### Live agent output (peek)

Both execution paths in `_agent.py` accept an `on_output_line(line, stream)` callback and drain the agent's stdout/stderr line-by-line — the blocking path uses two background reader threads, and the streaming path forwards each raw line from `_read_agent_stream`. The engine wires this callback to emit `EventType.AGENT_OUTPUT_LINE` events, which the `ConsoleEmitter` renders only while peek is enabled. The `p` keybinding flips that state via `ConsoleEmitter.toggle_peek()`, driven by `KeypressListener` in `_keypress.py`. The listener only activates on a real TTY; in CI or when stdin is piped it silently no-ops.

### Credit trailer

When `credit` is `true` (the default), `engine.py:_assemble_prompt()` appends `_CREDIT_INSTRUCTION` to the prompt — a short instruction telling the agent to include a `Co-authored-by: Ralphify` trailer in git commits. Users can opt out with `credit: false` in frontmatter.

### Subprocess result types

`_output.py` defines `ProcessResult`, the base dataclass for subprocess results (provides `returncode`, `timed_out`, and a `success` property). Both `_runner.py:RunResult` (command execution) and `_agent.py:AgentResult` (agent execution) extend it. If you add a new subprocess wrapper, inherit from `ProcessResult` to get consistent success/timeout semantics. The module also provides `ensure_str()` for bytes-to-string decoding, `collect_output()` for combining stdout+stderr, and `SUBPROCESS_TEXT_KWARGS` — the shared kwargs dict used by all `subprocess.Popen` calls to ensure consistent encoding and stream handling.

### If you change shutdown or signal handling...

Agent subprocesses run in their own process group (`start_new_session=True` on POSIX, configured via `_SESSION_KWARGS` in `_agent.py`). This lets `_kill_process_group()` send signals to the agent and all its children at once.

The two-stage Ctrl+C flow:

1. **First Ctrl+C** — the engine's SIGINT handler sets `RunState.stop_requested`, which lets the current iteration finish gracefully.
2. **Second Ctrl+C** — `KeyboardInterrupt` propagates normally and the agent process is killed.

Timeout and cancellation both use a two-step kill: SIGTERM first, then SIGKILL after `_SIGTERM_GRACE_PERIOD` seconds (3s). If you add a new subprocess wrapper, use `_kill_process_group()` and `_SESSION_KWARGS` to get consistent cleanup behavior.

### Command parsing

Commands in RALPH.md frontmatter are parsed with `shlex.split()` — no shell features. For shell features, users point the `run` field at a script.

## Testing

```bash
uv run pytest           # Run all tests
uv run pytest -x        # Stop on first failure
```

Tests are in `tests/` with one file per module. All tests use temporary directories and don't require any external services.

## Dependencies

Minimal by design:

- **typer** — CLI framework
- **rich** — Terminal formatting (used via typer's console)
- **pyyaml** — YAML frontmatter parsing in `_frontmatter.py`

Dev dependencies: pytest, mkdocs, mkdocs-material.
