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, new) — 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.* }})
├── _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
├── _skills.py # Skill installation and agent detection for `ralph new`
├── _console_emitter.py # Rich console renderer for run-loop events (ConsoleEmitter)
├── _events.py # Event types and emitter protocol (NullEmitter, QueueEmitter, FanoutEmitter)
├── _output.py # Combine stdout+stderr, format durations
└── skills/ # Bundled skill definitions (installed into agent skill dirs)
└── new-ralph/ # AI-guided ralph creation skill for `ralph new`
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.* }} and {{ args.* }} 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:
{{ commands.tests }}— replaced with the test command's output{{ args.dir }}— replaced with the user argument value- Unmatched placeholders resolve to empty string
Event system¶
The run loop communicates via structured events (_events.py). Each event has a type (EventType enum), run ID, data dict, and UTC timestamp.
EventEmitter— protocol that any listener implements (just anemit(event)method)NullEmitter— discards events (used in tests)QueueEmitter— pushes events into aqueue.Queuefor async consumptionFanoutEmitter— broadcasts events to multiple emitters
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
RunStatethread-safe control methods - Uses
FanoutEmitterto broadcast events to multiple listeners
Key files to understand first¶
engine.py— The core run loop. UsesRunConfigandRunState(from_run_types.py) andEventEmitter. This is where iteration logic lives._run_types.py—RunConfig,RunState,RunStatus, andCommand. These are the shared data types used by the engine, CLI, and manager.cli.py— All CLI commands. Delegates toengine.run_loop()for the actual loop. Terminal event rendering lives in_console_emitter.py._frontmatter.py— YAML frontmatter parsing. Extractsagent,commands,argsfrom the RALPH.md file.resolver.py— Template placeholder logic. Small file but critical._skills.py+skills/— The skill system behindralph new._skills.pyhandles agent detection, reads bundled skill definitions fromskills/, installs them into the agent's skill directory, and builds the command to launch the agent.
Traps and gotchas¶
If you change frontmatter fields...¶
Frontmatter parsing is in _frontmatter.py:parse_frontmatter(). The field names are consumed in cli.py and engine.py. Adding a new typed field may require updating the coercion logic in parse_frontmatter().
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. The ConsoleEmitter in _console_emitter.py renders them to the terminal. Adding a new event type requires handling it in ConsoleEmitter and any other active emitters.
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¶
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)
- No other runtime dependencies
Dev dependencies: pytest, mkdocs, mkdocs-material.