ralph-coder 0.2.1

An agentic code generation CLI powered by multiple LLM backends
Documentation
# Ralph — Project Plan

**Stack:** Rust
**Target:** v1.0 CLI release
**Approach:** Iterative milestones, each independently testable

---

## Milestones Overview

| # | Milestone | Deliverable |
|---|-----------|-------------|
| 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

| Decision | Choice | Rationale |
|----------|--------|-----------|
| 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

| Risk | Mitigation |
|------|-----------|
| 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