# Ralph — Project Plan
**Stack:** Rust
**Target:** v1.0 CLI release
**Approach:** Iterative milestones, each independently testable
---
## Milestones Overview
| 1 | Foundation | Compilable binary, CLI parsing, config loading, subcommand skeleton |
| 2 | Provider Layer | DeepSeek API client with tool use (+ additional providers in code) |
| 3 | Tool Engine | All 11 tools implemented and unit tested |
| 4 | Ralph Loop | Full observe → plan → execute → verify cycle |
| 5 | Guardrails | All hard blocks and soft guards active |
| 6 | Search Integration | Web search wired into all providers |
| 7 | Session Persistence | Auto-resume, session save/load, `sessions` subcommands |
| 8 | Context Compaction | Token counting, compaction trigger, LLM-driven summary |
| 9 | Checkpoints | Create, list, revert, auto-checkpoint, `checkpoint` subcommands |
| 10 | Output & UX | Terminal rendering, JSON output, logging, compaction/checkpoint events |
| 11 | Polish & Release | Error messages, README, cross-platform testing, packaging |
---
## Milestone 1 — Foundation
**Goal:** `ralph --help` and `ralph --version` work. Config loads. All subcommands are registered (stubs). Project structure established.
### Tasks
- [ ] Initialize `cargo new ralph --bin`
- [ ] Set up `Cargo.toml` with all dependencies
- [ ] Implement CLI argument parsing with `clap`
- `--deepseek` flag (and other provider flags present in code)
- `--model`, `--workspace`, `--max-turns`, `--dry-run`, `--no-confirm`, `--context`, `--output`, `--verbose`
- `--resume [session-id]`, `--new-session`, `--checkpoint <name>`, `--auto-checkpoint`
- Positional `[PROMPT]` argument
- Subcommands: `sessions {list,show,clean}` and `checkpoint {list,create,revert,show}` (stub implementations)
- [ ] Implement `config.rs`: load `~/.ralph/config.toml` and `.ralph.toml` with `toml` + `serde`
- Include `[session]`, `[compaction]`, `[checkpoints]` config sections
- [ ] Implement `errors.rs`: `RalphError` enum covering all error categories
- [ ] Validate API key presence at startup; exit with clear message if missing
- [ ] Write integration test: `ralph --help` exits 0; `ralph sessions list` exits 0 (stub)
**Exit Criteria:** Binary compiles, `--help` shows all flags and subcommands, missing key prints actionable error.
---
## Milestone 2 — Provider Layer
**Goal:** Each LLM provider can receive a message with tool definitions and return a structured tool-call response.
### Tasks
- [ ] Define `LlmProvider` trait in `providers/mod.rs`
```rust
trait LlmProvider {
async fn chat(&self, messages: &[Message], tools: &[ToolDef]) -> Result<LlmResponse>;
}
```
- [ ] Define shared types: `Message`, `ToolDef`, `ToolCall`, `LlmResponse`
- [ ] Implement `providers/deepseek.rs`
- Auth via `DEEPSEEK_API_KEY`
- Model: `deepseek-chat`
- DeepSeek uses OpenAI-compatible API — reuse OpenAI client with base URL override
- [ ] Implement retry logic: exponential backoff on 429/500 errors (max 3 attempts)
- [ ] Unit tests: mock HTTP responses for each provider, verify correct request format and tool call parsing
**Exit Criteria:** Each provider sends a correctly formatted request and parses a tool-call response from a mock server.
---
## Milestone 3 — Tool Engine
**Goal:** All tools execute correctly and return structured results. Dangerous tools block pending confirmation.
### Tasks
- [ ] Define `ToolCall`, `ToolResult`, `ToolRegistry` types in `tools/mod.rs`
- [ ] Implement `tools/file_tools.rs`
- `read_file(path)` — reads relative to workspace root
- `list_dir(path)` — uses `ignore` crate to respect `.gitignore`
- `write_file(path, content)` — creates parent dirs as needed
- `edit_file(path, old_string, new_string)` — fails if `old_string` not found
- `delete_file(path)`
- [ ] Implement `tools/shell_tools.rs`
- `run_command(cmd, cwd)` — executes via `tokio::process::Command`
- `run_test(cmd)` and `run_build(cmd)` (whitelisted variants)
- Capture stdout + stderr, return combined with exit code
- Enforce working directory is within workspace
- [ ] Implement `tools/search_tools.rs`
- `search_web(query)` — calls Brave Search API (`BRAVE_API_KEY`) or SerpAPI (`SERP_API_KEY`); returns top-5 snippets
- `search_codebase(pattern, path)` — regex search via `regex` crate over workspace files
- [ ] Implement `tools/meta_tools.rs`
- `ask_user(question)` — suspends loop, prints question, reads stdin
- `declare_done(summary)` — sets loop exit flag with success
- `declare_failed(reason)` — sets loop exit flag with failure
- [ ] Unit tests for every tool (file operations on a temp directory, shell on a known command)
**Exit Criteria:** All tools pass unit tests. Confirmation prompt appears correctly for destructive tools.
---
## Milestone 4 — Ralph Loop
**Goal:** Full agentic loop runs end-to-end on a simple task.
### Tasks
- [ ] Implement `context.rs`: `WorkspaceContext` builder
- Collect file tree (depth-limited, respects `.gitignore`)
- Collect git status if in a git repo
- Collect content of explicitly referenced files
- [ ] Implement `prompts.rs`: System prompt templates
- Base system prompt with tool descriptions
- Search instruction section (always present)
- Guardrail instruction section
- [ ] Implement `loop_runner.rs`: `RalphLoop` struct
- `observe()` — build workspace context
- `plan()` — call LLM with context + tool defs, get back tool calls
- `execute()` — dispatch tool calls through `ToolRegistry`
- `verify()` — call LLM with tool results, decide next action
- Loop orchestration with turn counter
- `--dry-run` support: print planned tool calls, skip execute
- [ ] Wire everything in `main.rs`
- [ ] Integration test: run Ralph on a trivial task (e.g. "create a file hello.txt with content 'hello'") in a temp workspace
**Exit Criteria:** Ralph completes the integration test in 1-2 turns and the file exists with correct content.
---
## Milestone 5 — Guardrails
**Goal:** All hard blocks and soft guards are enforced. No workspace escapes. No dangerous commands run unconfirmed.
### Tasks
- [ ] Implement `guardrails.rs`: `GuardrailChecker`
- Path containment check: reject any path outside workspace root (resolve symlinks)
- Blocked command patterns: check `run_command` against blocklist from config
- Secret detection: scan `write_file` content for patterns matching API keys, passwords, private keys
- Env var allowlist enforcement
- [ ] Integrate `GuardrailChecker` into `ToolRegistry` — called before every tool execution
- [ ] Implement soft-guard confirmation flow in `output.rs`
- Prompt with `[y/N/d]` options
- `d` generates a diff via `similar` crate and displays it
- [ ] Token budget tracking: count tokens per turn (estimate via character count / 4), warn at 80%, halt at 100%
- [ ] Tests: verify path traversal attempts are blocked, blocked commands are rejected, secret patterns are caught
**Exit Criteria:** All guardrail tests pass. Attempting `../../etc/passwd` path is rejected with clear error.
---
## Milestone 6 — Search Integration
**Goal:** Ralph searches the web when the LLM requests it, and the prompt actively encourages search before assumptions.
### Tasks
- [ ] Finalize search-instruction section in `prompts.rs`
- Explicit instructions to call `search_web` before using any unfamiliar API
- Instructions to verify library versions match project's dependency manifest
- Instructions to search error messages encountered in `run_command` output
- [ ] Complete `search_web` implementation
- Brave Search API integration with `BRAVE_API_KEY`
- Format results as concise snippets with source URL
- Graceful degradation: if key absent, return informational message to LLM
- [ ] Integration test: task that requires knowing the latest version of a crate — verify LLM uses `search_web`
**Exit Criteria:** Integration test passes. LLM-initiated search returns usable results within the loop.
---
## Milestone 7 — Session Persistence
**Goal:** Sessions are saved after every turn. Ralph auto-detects and offers to resume the previous session on startup. `sessions` subcommands work.
### Tasks
- [ ] Implement `session.rs`
- `Session` struct with all `session.json` fields
- `Session::create(workspace, provider, model)` — generates session ID, writes `session.json` and `messages.json`
- `Session::load(session_id)` — deserializes from disk
- `Session::find_for_workspace(path)` — finds most recent matching session
- `Session::save_turn(messages, turn_count)` — atomic write to `messages.json` + update `session.json`
- `Session::set_status(status)` — update `status` field (`in_progress`, `done`, `failed`, `interrupted`)
- [ ] Implement startup resume flow in `main.rs`
- Check for existing session; prompt if `auto_resume = true`
- Load message history; inject into LLM context as prior conversation
- `--resume` flag skips prompt; `--new-session` skips lookup entirely
- [ ] Implement `sessions list` subcommand: show session ID, workspace, provider, turn count, last active, status
- [ ] Implement `sessions show <id>` subcommand: full session detail
- [ ] Implement `sessions clean` subcommand: delete sessions older than `clean_after_days`
- [ ] Handle Ctrl+C: catch signal, set status to `interrupted`, flush session state before exit
- [ ] Unit tests: create session, save turns, load back, verify data integrity
**Exit Criteria:** Running `ralph --anthropic "task"`, Ctrl+C, then `ralph --anthropic --resume "continue"` resumes with full history. `ralph sessions list` shows the session.
---
## Milestone 8 — Context Compaction
**Goal:** Long sessions are automatically compacted when approaching the context window limit without losing meaningful context.
### Tasks
- [ ] Add `tiktoken-rs` dependency for accurate token counting
- [ ] Implement `compaction.rs`
- `TokenCounter`: count tokens in a message list per provider's tokenizer
- `should_compact(messages, config) -> bool`: check if threshold exceeded
- `compact(messages, provider, config) -> Result<(Vec<Message>, ArchiveMessages)>`
- Sends full history to LLM with compaction prompt
- Returns: compacted window (summary + last N turns) + full archive
- [ ] Integrate compaction into the OBSERVE phase of `loop_runner.rs`
- Call `should_compact` before each LLM call
- If true, run `compact`, update in-memory messages, write `compacted.json`, emit `[compact]` event
- [ ] Add provider context window constants to `providers/mod.rs`
- [ ] Unit tests: construct a token-heavy message list, verify compaction fires at threshold, verify archive is preserved
**Exit Criteria:** A session with 150+ KB of message history compacts cleanly. The compacted window fits within the provider limit. `compacted.json` contains the full original history.
---
## Milestone 9 — Checkpoints
**Goal:** Users can create named checkpoints during a session and revert workspace files and conversation history to any prior checkpoint.
### Tasks
- [ ] Implement `checkpoint.rs`
- `Checkpoint` struct: id, name, turn, timestamp, list of modified file paths
- `Checkpoint::create(session, name, workspace_root)` — snapshot current `messages.json` + all files Ralph has touched
- `Checkpoint::load(session_id, checkpoint_id)` — deserialize from disk
- `Checkpoint::list(session_id)` — return sorted list
- `Checkpoint::revert(session, checkpoint, files_only)` — restore files + optionally messages
- Always creates a `before-revert-<timestamp>` checkpoint first
- Shows diff preview before proceeding
- Requires explicit confirmation (not bypassable)
- [ ] Track modified files in `Session`: maintain `modified_files: Vec<PathBuf>` updated on each `write_file` / `edit_file` / `delete_file`
- [ ] Wire `--checkpoint <name>` flag: create checkpoint at session start (turn 0) before any changes
- [ ] Wire `--auto-checkpoint` flag + config: create checkpoint before any `write_file` or `delete_file` tool call
- [ ] Add `c` option to soft-guard confirmation prompt (checkpoint then proceed)
- [ ] Implement `checkpoint list`, `checkpoint create`, `checkpoint revert`, `checkpoint show` subcommands
- [ ] Tests:
- Create session, modify files, create checkpoint, modify more files, revert, verify file contents and message state
- Verify `before-revert-<timestamp>` checkpoint is always created
- Verify revert cannot be skipped with `--no-confirm`
**Exit Criteria:** `ralph checkpoint revert before-auth-refactor` restores files and conversation to the named checkpoint. The revert is itself reversible via the auto-created pre-revert checkpoint.
---
## Milestone 10 — Output & UX
**Goal:** Terminal output is clear, structured, and color-coded. JSON mode works. Session logs are written. Compaction and checkpoint events are surfaced.
### Tasks
- [ ] Implement `output.rs`
- Phase prefixes: `[observe]`, `[plan]`, `[tool]`, `[verify]`, `[compact]`, `[checkpoint]`, `[ralph]`, `[done]`, `[failed]`
- Colors via `colored` crate: phases in bold, tool names in cyan, errors in red, prompts in yellow, compaction in magenta, checkpoints in blue
- `--verbose` mode: show full LLM message content
- `--output json`: emit one JSON object per event to stdout (includes `session_id`)
- [ ] Implement session log writer: `~/.ralph/logs/YYYY-MM-DD_<session-id>.log`
- [ ] Progress indicator via `indicatif` during LLM API calls
- [ ] Pretty-print diffs in confirmation prompts and checkpoint revert previews using `similar`
- [ ] Test: run with `--output json`, pipe to `jq`, verify schema
**Exit Criteria:** Terminal output is legible and consistently formatted. JSON output parses correctly. Compaction and checkpoint events appear in both terminal and JSON output.
---
## Milestone 11 — Polish & Release
**Goal:** Production-quality CLI ready for distribution.
### Tasks
- [ ] Write `README.md`: installation, quickstart, all flags, env vars, config file reference, session/checkpoint workflow
- [ ] Complete `--context <file>` support: inject file contents into initial LLM context
- [ ] Improve error messages: every `RalphError` variant has a user-facing suggestion
- [ ] Add `ralph --check`: validate config and API key presence without running a task
- [ ] Cross-platform testing: macOS, Linux (Ubuntu), Windows (best-effort)
- [ ] Release build: `cargo build --release`, strip binary
- [ ] Package: Homebrew formula, `cargo install` via crates.io (optional)
- [ ] Final end-to-end integration tests covering session resume, compaction, and checkpoint revert
**Exit Criteria:** `cargo install ralph` works. README covers all user-facing features including session and checkpoint workflows. All tests pass.
---
## Dependency Graph
```
M1 (Foundation)
└── M2 (Provider Layer)
└── M3 (Tool Engine)
├── M4 (Ralph Loop) ←── depends on M2 + M3
│ ├── M5 (Guardrails) ← can start in parallel with M4
│ │ └── M6 (Search Integration)
│ └── M7 (Session Persistence) ←── depends on M4
│ └── M8 (Context Compaction) ←── depends on M7 + M2
│ └── M9 (Checkpoints) ←── depends on M7 + M3
│ └── M10 (Output & UX) ←── depends on M7–M9
│ └── M11 (Polish)
└── M5 (Guardrails) ← can start in parallel with M4
```
> M7 (Session) and M5 (Guardrails) can be developed in parallel once M4 is complete.
> M8 (Compaction) and M9 (Checkpoints) can be developed in parallel once M7 is complete.
---
## Key Design Decisions
| Async runtime | `tokio` | Industry standard; needed for concurrent LLM + tool execution |
| HTTP client | `reqwest` | Ergonomic async client, TLS built-in |
| CLI parsing | `clap` v4 derive macros | Type-safe, auto-generates `--help` |
| Config format | TOML | Human-readable, well-supported in Rust ecosystem |
| Error handling | `anyhow` + `thiserror` | `thiserror` for library errors, `anyhow` for application context |
| Tool dispatch | Enum + match | Avoids dynamic dispatch overhead, exhaustiveness checked at compile time |
| DeepSeek client | Reuse OpenAI client | DeepSeek API is OpenAI-compatible; no duplication |
| Session storage | Flat JSON files in `~/.ralph/sessions/` | Simple, human-inspectable, no database dependency |
| Checkpoint file snapshots | Copy-on-write file snapshots | Simple restore path; no git dependency required |
| Token counting | `tiktoken-rs` | Accurate per-provider counts; avoids premature compaction |
| Compaction strategy | LLM-generated summary | Preserves semantic meaning better than truncation |
---
## Risks & Mitigations
| LLM generates malformed tool calls | Retry once with error context; fail gracefully after 2 attempts |
| Brave Search API unavailable | Degradation message to LLM; continue without search |
| Token budget exhaustion on large workspaces | Depth-limited file tree; truncate large files in context |
| Provider API changes | Abstract behind `LlmProvider` trait; each provider isolated |
| Path traversal attacks in tool calls | Resolved-path containment check in `guardrails.rs` before every file op |
| Session file corruption | Validate JSON schema on load; offer to start fresh if corrupt |
| Compaction loses important context | Keep last N turns verbatim; full history archived in `compacted.json` |
| Checkpoint disk usage on large projects | Only snapshot files Ralph has actually modified; warn if snapshot exceeds configurable size limit |
| Concurrent ralph instances on same workspace | Lock file in session directory; second instance prompts user |
---
## Success Criteria for v1.0
1. `ralph --deepseek "add a fibonacci function to src/lib.rs"` completes successfully on a Rust project
2. `ralph --deepseek "fix the failing tests"` searches the web for relevant API docs when needed
3. Attempting to escape the workspace is blocked
5. `--dry-run` shows planned actions without touching the filesystem
6. Session log is written and parseable
7. Ctrl+C mid-session, re-running `ralph --deepseek --resume "continue"` restores full conversation history
8. After 10+ turns on a large codebase, compaction fires automatically and the session continues without error
9. `ralph checkpoint revert <name>` restores both files and conversation to a named prior state
10. The revert is itself reversible via the auto-created pre-revert checkpoint