Primitives¶
Primitives are reusable building blocks that extend your loop. They live in the .ralphify/ directory and are automatically discovered by ralphify.
There are four kinds:
| Primitive | Purpose | Runs when |
|---|---|---|
| Checks | Validate the agent's work (tests, linters) | After each iteration |
| Contexts | Inject dynamic data into the prompt | Before each iteration |
| Instructions | Inject static text into the prompt | Before each iteration |
| Ralphs | Reusable task-focused ralphs you can switch between | At run start |
Checks¶
Checks run after each iteration to validate what the agent did. If a check fails, its output (and optional failure instructions) are appended to the next iteration's prompt so the agent can fix the problem.
Creating a check¶
This creates .ralphify/checks/my-tests/CHECK.md:
Edit the frontmatter to set your validation command:
---
command: pytest -x
timeout: 120
enabled: true
---
Fix all failing tests. Do not skip or delete tests.
The body text below the frontmatter is the failure instruction — it gets included in the prompt alongside the check output when the check fails. Use it to tell the agent how you want failures handled.
Frontmatter fields¶
| Field | Type | Default | Description |
|---|---|---|---|
command |
string | — | Command to run (see command parsing below) |
timeout |
int | 60 |
Max seconds before the check is killed |
enabled |
bool | true |
Set to false to skip without deleting |
Checks need a command or script
A check must have either a command in its frontmatter or an executable run.* script in its directory. Checks that have neither are skipped with a warning during discovery. If ralph status shows fewer checks than you expect, verify each check has a command or script configured.
Command parsing¶
Commands are split with Python's shlex.split() and executed directly — not through a shell. This means:
- Simple commands work as expected:
uv run pytest -x,npm test,ruff check . - Shell features like pipes (
|), redirections (2>&1,>), chaining (&&,||), and variable expansion ($VAR) do not work - Arguments with spaces need quoting:
pytest "tests/my dir/"works correctly
If you need shell features, use a script instead.
Using a script instead of a command¶
Instead of a command in frontmatter, you can place an executable script named run.* (e.g. run.sh, run.py) in the check directory:
If both a command and a run.* script exist, the script takes precedence. Scripts and commands always run with the project root as the working directory, not the primitive's directory.
HTML comments are stripped¶
You can use HTML comments in any primitive file for internal notes — they're stripped before the content is injected into the prompt:
---
command: pytest -x
timeout: 120
enabled: true
---
<!-- TODO: consider adding --tb=short flag -->
<!-- Agreed on this policy in sprint retro 2025-01-10 -->
Fix all failing tests. Do not skip or delete tests.
The agent never sees the comments. This is useful for documenting why a check exists or what you've tried.
How check failures appear in the prompt¶
When a check fails, ralphify appends a section like this to the next iteration's prompt:
## Check Failures
The following checks failed after the last iteration. Fix these issues:
### my-tests
**Exit code:** 1
```
FAILED tests/test_foo.py::test_bar - AssertionError
```
Fix all failing tests. Do not skip or delete tests.
Contexts¶
Contexts inject dynamic data into the prompt before each iteration. Use them to give the agent fresh information like recent git history, open issues, or file listings.
Creating a context¶
This creates .ralphify/contexts/git-log/CONTEXT.md:
The command runs each iteration and its stdout is injected into the prompt.
Static content¶
The body below the frontmatter is static content that gets included above the command output:
---
command: git log --oneline -10
timeout: 30
enabled: true
---
## Recent commits
Here are the latest commits for reference:
A context can also be purely static (no command) — just omit the command field and write the content in the body.
Frontmatter fields¶
| Field | Type | Default | Description |
|---|---|---|---|
command |
string | — | Command whose stdout is captured (see command parsing) |
timeout |
int | 30 |
Max seconds before the command is killed |
enabled |
bool | true |
Set to false to skip without deleting |
Using a script instead of a command¶
Just like checks, you can place an executable script named run.* (e.g. run.sh, run.py) in the context directory instead of using a command in frontmatter:
If both a command and a run.* script exist, the script takes precedence. Scripts and commands always run with the project root as the working directory.
This is useful for contexts that need more complex logic than a single shell command — for example, querying an API, combining multiple data sources, or running a Python script that formats output.
Placement in the prompt¶
By default, all context output is appended to the end of the prompt. To control where it appears, use placeholders in your RALPH.md:
{{ contexts.git-log }}— places that specific context's output here{{ contexts }}— places all remaining contexts (those not already placed by name)- If no placeholders are found, all context output is appended to the end
Instructions¶
Instructions inject static text into the prompt. Use them for reusable rules, style guides, or constraints that you want to add or remove without editing the ralph file.
Creating an instruction¶
This creates .ralphify/instructions/code-style/INSTRUCTION.md:
Write your instruction content in the body:
---
enabled: true
---
Always use type hints on function signatures.
Keep functions under 30 lines.
Never use print() for logging — use the logging module.
Empty instructions are excluded
Instructions with no body text (only frontmatter) are silently excluded from prompt injection, even when enabled: true. If an instruction isn't appearing in your prompt, make sure it has content below the frontmatter.
Frontmatter fields¶
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Set to false to skip without deleting |
Placement in the prompt¶
Same rules as contexts:
{{ instructions.code-style }}— places that specific instruction here{{ instructions }}— places all remaining instructions- If no placeholders are found, all instructions are appended to the end
Ralphs¶
Ralphs are reusable, named ralph files that let you switch between different tasks without editing your root RALPH.md. Instead of maintaining one ralph and rewriting it each time you change focus, you create named ralphs and select the one you want at run time.
When to use named ralphs¶
Named ralphs are useful when you have multiple recurring tasks for the same project:
- A
docsralph for documentation improvements - A
refactorralph for cleaning up code - A
add-testsralph for increasing test coverage - A
bug-fixralph for systematic bug fixing
Each ralph can have its own placeholders, constraints, and workflow — tailored to that specific job.
Creating a ralph¶
This creates .ralphify/ralphs/docs/RALPH.md:
Edit it with your task-specific prompt:
---
description: Improve project documentation
enabled: true
---
# Ralph
You are a documentation agent. Each iteration starts fresh.
Read the codebase and existing docs. Find the biggest gap between
what the code can do and what the docs explain. Write or improve
one page per iteration.
- Search before creating new files
- No placeholder content — full, accurate writing only
- Verify code examples actually work
- Commit with `docs: <what you documented>`
{{ contexts }}
{{ instructions }}
Frontmatter fields¶
| Field | Type | Default | Description |
|---|---|---|---|
description |
string | "" |
Short description shown in ralph status |
enabled |
bool | true |
Set to false to hide without deleting |
Running a named ralph¶
Pass the ralph name as the first argument to ralph run:
You can also set a default ralph in ralph.toml:
[agent]
command = "claude"
args = ["-p", "--dangerously-skip-permissions"]
ralph = "docs" # Name of a ralph in .ralphify/ralphs/
When ralph is set to a name (no / or . in the value), ralphify looks for .ralphify/ralphs/<name>/RALPH.md first, then falls back to treating it as a file path.
Listing ralphs¶
Use ralph status to see all discovered ralphs with their enabled status and descriptions.
Priority chain¶
When you run ralph run, the prompt is resolved in this order (first match wins):
-pflag — inline ad-hoc prompt text- Positional argument —
ralph run <name>looks up.ralphify/ralphs/<name>/RALPH.md --prompt-file/-fflag — explicit path to a prompt fileralph.tomlralphfield — can be a name or a file path- Fallback —
RALPH.mdin the project root
Named ralphs support all the same features as the root RALPH.md: context and instruction placeholders resolve as normal, and check failures are appended after each iteration.
Named ralphs also support ralph-scoped primitives — checks, contexts, and instructions that only apply when running that specific ralph.
Ralph-scoped primitives¶
When you use named ralphs, you can attach checks, contexts, and instructions to a specific ralph. These ralph-scoped primitives live inside the ralph's directory and are merged with your global primitives when that ralph runs.
Why use them¶
Different tasks need different validation. A documentation ralph might need a mkdocs build check but not a cargo test check. A refactoring ralph might need stricter lint rules. Ralph-scoped primitives let you customize the loop per task without cluttering the global .ralphify/ directory.
Creating ralph-scoped primitives¶
Use the --ralph flag with ralph new to scaffold a primitive inside a named ralph's directory:
ralph new check docs-build --ralph docs
ralph new context doc-coverage --ralph docs
ralph new instruction writing-style --ralph docs
This creates the primitive inside .ralphify/ralphs/docs/ instead of the global .ralphify/ directory.
Directory structure¶
Place primitive directories inside the named ralph's directory, using the same checks/, contexts/, instructions/ layout:
.ralphify/ralphs/docs/
├── RALPH.md
├── checks/
│ └── docs-build/
│ └── CHECK.md ← only runs with the "docs" ralph
├── contexts/
│ └── doc-coverage/
│ └── CONTEXT.md ← only injected with the "docs" ralph
└── instructions/
└── writing-style/
└── INSTRUCTION.md ← only included with the "docs" ralph
How merging works¶
When you run ralph run docs, ralphify discovers both global and ralph-scoped primitives, then merges them:
- Global primitives from
.ralphify/checks/,.ralphify/contexts/,.ralphify/instructions/are loaded first - Ralph-scoped primitives from
.ralphify/ralphs/docs/checks/, etc. are loaded next - If a local primitive has the same name as a global one, the local version wins
- Enabled filtering happens after the merge — a disabled local primitive can suppress a global one
This means you can:
- Add ralph-specific primitives that only run for that ralph
- Override a global primitive by creating a local one with the same name
- Suppress a global primitive by creating a disabled local one with the same name
Example: override a global check¶
Say you have a global test check:
For your docs ralph, you want to skip full tests and only validate the docs build. Create a local override:
This disables the global tests check when running the docs ralph, because the local primitive with the same name (tests) takes precedence.
Then add a ralph-specific check:
Now ralph run docs runs only the docs-build check (plus any other global checks not overridden).
With ad-hoc prompts¶
Ralph-scoped primitives only apply when running a named ralph. Ad-hoc prompts (ralph run -p "...") use global primitives only, since there is no ralph directory to scan.
Behavior notes¶
Important runtime behaviors that affect how you design and organize your primitives.
Execution order¶
Primitives are discovered and executed in alphabetical order by directory name. This applies to checks, contexts, and instructions alike.
If execution order matters — for example, you want a fast lint check to run before a slow test suite — use number prefixes:
.ralphify/checks/
├── 01-lint/ ← runs first (fast feedback)
│ └── CHECK.md
├── 02-typecheck/ ← runs second
│ └── CHECK.md
└── 03-tests/ ← runs last (slowest)
└── CHECK.md
All checks run regardless of whether earlier checks pass or fail — there is no short-circuiting.
Naming¶
A primitive's name is its directory name, not a field in frontmatter. The name tests comes from the directory .ralphify/checks/tests/, not from anything inside CHECK.md. This name is used in:
ralph statusoutput- Check failure headings in the prompt
- Placeholder references like
{{ contexts.git-log }}
Frontmatter format¶
Frontmatter uses a simplified key: value format — not full YAML. Each line is one field:
Limitations:
- No nested structures, lists, or multi-line values
- Lines starting with
#are treated as comments and ignored - Only
timeout(coerced to int) andenabled(coerced to bool) have type coercion — all other fields are strings - Values for
enabledare truthy if they matchtrue,yes, or1(case-insensitive)
Context command failures¶
Context output is injected into the prompt regardless of the command's exit code. Even if a context command exits non-zero, its stdout and stderr are still captured and included. This is intentional — commands like pytest --tb=line -q often exit non-zero (because tests are failing) but produce exactly the output you want the agent to see.
If a context command produces no output at all, only its static content (the body below the frontmatter) is injected. If it has neither output nor static content, it contributes nothing to the prompt.
What's re-read vs. fixed at startup¶
| What | When it's loaded | Editable while running? |
|---|---|---|
RALPH.md |
Every iteration | Yes — edits take effect next iteration |
| Context command output | Every iteration | Yes — commands re-run each time |
| Context/instruction config | Startup only | No — restart the loop |
| Check config | Startup only | No — restart the loop |
| New/removed primitives | Startup only | No — restart the loop |
RALPH.md is the primary way to steer the agent in real time. To add or modify primitives, stop the loop (Ctrl+C) and restart.
Disabled primitives¶
Setting enabled: false in frontmatter skips the primitive during execution but does not hide it. Disabled primitives still appear in ralph status (marked with a different indicator) and are still discovered — they're just filtered out before running. This makes it easy to toggle primitives on and off without deleting directories.
Directory structure¶
.ralphify/
├── checks/ ← global checks (all ralphs)
│ ├── lint/
│ │ └── CHECK.md
│ └── tests/
│ ├── CHECK.md
│ └── run.sh
├── contexts/ ← global contexts
│ └── git-log/
│ └── CONTEXT.md
├── instructions/ ← global instructions
│ └── code-style/
│ └── INSTRUCTION.md
└── ralphs/
├── docs/
│ ├── RALPH.md
│ ├── checks/ ← ralph-scoped (docs only)
│ │ └── docs-build/
│ │ └── CHECK.md
│ └── instructions/
│ └── writing-style/
│ └── INSTRUCTION.md
└── refactor/
└── RALPH.md
Viewing your primitives¶
Use ralph status to see all discovered primitives and whether they're enabled: