diff --git a/.cursorindexingignore b/.cursorindexingignore new file mode 100644 index 00000000..72e8ffc0 --- /dev/null +++ b/.cursorindexingignore @@ -0,0 +1 @@ +* diff --git a/.github/workflows/refresh-seed.yml b/.github/workflows/refresh-seed.yml new file mode 100644 index 00000000..ed039be0 --- /dev/null +++ b/.github/workflows/refresh-seed.yml @@ -0,0 +1,56 @@ +name: Refresh pricing seed + +# Keep the offline pricing floor (assets/pricing/models-dev-seed.json) current +# without hand-editing it (ADR-0034 A3, issue #290). The generator refreshes the +# seed from live models.dev out-of-band — never build.rs — and this job opens a +# diffable PR only when the numbers actually change. Cadence is decoupled from +# the release cycle. HITL: the resulting PR is reviewed as data before it merges. + +on: + schedule: + # Weekly, Mondays 06:00 UTC. + - cron: "0 6 * * 1" + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + +permissions: + contents: write # push the refresh branch + pull-requests: write # open the review PR + +jobs: + refresh: + name: regenerate seed · open PR on change + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry and target + uses: Swatinem/rust-cache@v2 + + - name: Regenerate seed from live models.dev + run: cargo run -p xtask -- refresh-seed + + - name: Open a diffable PR when the seed changed + # No-op safe: with no diff the action creates/updates no PR. + uses: peter-evans/create-pull-request@v6 + with: + add-paths: assets/pricing/models-dev-seed.json + branch: chore/refresh-pricing-seed + delete-branch: true + commit-message: "chore(pricing): refresh models.dev seed floor" + title: "chore(pricing): refresh models.dev seed floor" + body: | + Automated refresh of the offline pricing seed + (`assets/pricing/models-dev-seed.json`) from live models.dev, + narrowed to the driven providers (anthropic, openai, google, + moonshotai). Review the price deltas as data before merging — a + deliberate floor above upstream (e.g. `claude-opus-4-8`, ADR-0008 D8) + should be restored here rather than let the refresh regress it, and + the `floor.rs` golden values move with any accepted change. + + Generated by `cargo run -p xtask -- refresh-seed` (issue #290). diff --git a/CLAUDE.md b/CLAUDE.md index ba0b4b82..70c6a5a1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,9 +9,33 @@ them here. is defined there. Use these words; don't invent synonyms. - **[docs/adr/](./docs/adr/)** — architecture decisions. Check for a relevant ADR before changing a seam; the boundary you're about to cross was probably - decided on purpose (e.g. ADR-0004 core/adapter boundary). + decided on purpose (e.g. ADR-0002 core/adapter boundary). - **[docs/BUILDING.md](./docs/BUILDING.md)** — build, CI, crate layout. +## Architecture — ports & adapters, ubiquitous-language-first + +Ralphy is **hexagonal (ports & adapters)** at the crate seam, with DDD in its +**tactical** sense only: the [CONTEXT.md](./CONTEXT.md) glossary *is* the +ubiquitous language and each crate is roughly one bounded context. There is no +strategic-DDD ceremony here — no aggregates, repositories, or domain-event +buses. Don't add them. + +- **`ralphy-core` is the center and depends on no vendor.** It defines the agent + contract (the *port*) and owns queue lifecycle, git/forge, run reporting. It + must never gain a dependency on a `ralphy-agent-*` crate or on + `ralphy-adapter-support`; the dependency arrow points *inward*, toward core + ([ADR-0002](./docs/adr/0002-core-agnostic-adapter-boundary.md) protects this + seam). If core seems to need something vendor-specific, the design is wrong — + lift it behind the contract, don't leak it in. +- **Each `ralphy-agent-*` is an adapter** implementing that port: one crate per + vendor, holding all that is vendor-specific (execution mode, completion + protocol). **`ralphy-adapter-support`** is the vendor-*neutral* plumbing the + adapters share — it produces no `Outcome` (CONTEXT.md → *Adapter support*). +- **`ralphy-cli` is the composition root** — the one place that names every + vendor and wires them together. Vendor enumeration lives *there and only + there* (plus the [ADR-0040](./docs/adr/0040-agent-adapter-onboarding-contract.md) + inventory), never scattered across the tree. + ## Hard rules (an agent will get these wrong without being told) - **The green gate is CI's gate.** Before considering a change done: @@ -28,17 +52,84 @@ them here. [ADR-0022](./docs/adr/0022-file-split-conventions.md): `foo.rs` + `foo/` layout (never `mod.rs`), tests migrate with the code, split by existing responsibility only. +- **Tests live next to what they test — separated by `#[cfg(test)]`, not by a + parallel source tree.** That is this repo's convention *and* idiomatic Rust, + and `#[cfg(test)]` compiles the code out of release builds, so nothing + test-only ever ships — that gate is the "don't mix production and tests" + guarantee, not a separate root. Placement: + *unit tests* stay in the same crate as the code, either an inline + `#[cfg(test)] mod tests` or — once a file splits (ADR-0022) — a sibling + `#[cfg(test)]` submodule file (`foo/tests.rs`, or a named one like + `runstate/roundtrip.rs`); *integration tests* (black-box, public API only) go + in the crate's `tests/`, with data under `tests/fixtures/`; a **test helper + child binary** goes in `src/bin/_test_child.rs`, because its + `CARGO_BIN_EXE_*` is visible only to integration tests (CONTEXT.md → + *Testing conventions*). +- **Smallest change that fits the existing seam.** A new trait, generic, crate, + or layer of indirection needs a real second caller or a deciding ADR — never + "for flexibility" (`anti-over-abstraction`). Cross a seam only where an ADR + says to; if no ADR covers the boundary you're about to add, the change is + probably in the wrong place, or the seam is a design decision that wants an + ADR first. +- **English is the canonical written language.** ADRs, docs, GitHub issues and + PRs, commit messages and code comments are written in English, whatever + language the request arrived in. A conversation with a maintainer may be in + any language; the artifact is English. Issues in particular are work orders an + agent consumes, and they quote English ADRs, identifiers and paths — prose in + a second language makes one document speak two per sentence. - **Contributing to this repo:** commit on a branch; a human reviews and merges. Do not push or open a PR unless explicitly asked. (This mirrors Ralphy's own product ethos — it never pushes and never opens PRs.) +## Rust baseline (the always-on floor) + +The full `/rust-skills` (179 rules) is a surgical tool — invoke it to review +non-trivial code or a specific concern. These few are the minimum that hold +without invoking anything; they apply to every change. Each names the underlying +rule so `/rust-skills ` gives you the bad/good example on demand. + +- **Errors — this codebase is a subprocess driver, so errors are the hot path.** + No `.unwrap()`/`.expect()` on anything recoverable (spawn, I/O, git, network, + parse); `expect()` is allowed *only* for a violated invariant that is a bug, + and its message states why the invariant holds (`anti-unwrap-abuse`, + `anti-panic-expected`, `err-expect-bugs-only`). Never swallow an error — no + `let _ = result`, no bare `.ok()`, no empty `if let Err(_)`: handle or + propagate (`anti-empty-catch`). Propagate with `?` and add + `.context()`/`.with_context()` at each boundary so the chain reads + "what failed: why" (`err-context-chain`). Error messages start lowercase with + no trailing punctuation — they get chained (`err-lowercase-msg`). `anyhow` at + the app/composition boundary; a `thiserror` domain type at a seam callers must + match on (`err-anyhow-app`, `err-custom-type`). +- **Signatures — free flexibility clippy would flag anyway.** Take `&str` not + `&String`, `&[T]` not `&Vec` (`anti-string-for-str`, `anti-vec-for-slice`). + A fixed set of values or a semantic identity is an `enum`/newtype, not a + `String` — this is the CONTEXT.md ubiquitous language expressed in the type + system (`anti-stringly-typed`). +- **Idiom & restraint.** Iterators over manual `for i in 0..len` indexing; don't + `.collect()` mid-chain (`anti-index-over-iter`, `anti-collect-intermediate`). + `impl Trait` over `Box` when the type is concrete; start concrete + and generalize on a real second use, not "for flexibility" (`anti-type-erasure`, + `anti-over-abstraction`). No optimization without a profile + (`anti-premature-optimize`). +- **Async (daemon only).** Never hold a lock guard across an `.await`; use + `tokio::sync` primitives and drop the guard first (`anti-lock-across-await`). + ## Where things live `crates/ralphy-cli` (the `ralphy` binary + composition root) · `crates/ralphy-core` (queue lifecycle, git/GitHub, run reporting) · -`crates/ralphy-agent-{claude,codex,opencode}` (the vendor adapters) · +`crates/ralphy-agent-*` (**the vendor adapters — one crate per vendor**; +`claude`, `codex`, `kimi`, `opencode` today, more arriving) · `crates/ralphy-adapter-support` (vendor-neutral child-driving plumbing) · -`crates/ralphy-pty` · `assets/prompts` (plan/execute charters) · +`crates/ralphy-daemon` (the supervised launcher + workbench) · +`crates/ralphy-usage-scan` (stateless reads of the vendors' session stores) · +`crates/ralphy-pty` · `crates/ralphy-proc-util` · +`assets/prompts` (plan/execute charters) · `assets/plugin` (bundled skills, embedded into the binary). -#teste 1 \ No newline at end of file +Adding a vendor is not just a new crate: follow +[ADR-0040](./docs/adr/0040-agent-adapter-onboarding-contract.md), whose wiring +inventory lists every edit site across five tiers. **Do not enumerate the vendor +crates anywhere a list can go stale** — that list has already drifted once +(Kimi was missing from this section and is still missing from the daemon's +agent enum). \ No newline at end of file diff --git a/CONTEXT.md b/CONTEXT.md index 42db26bd..569bfe9f 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -125,14 +125,16 @@ _Avoid_: model selection (reserved for Claude complexity routing). The shared machinery every **adapter** leans on but that is specific to *no* vendor — the headless child-driving loop (spawn, drain stdout/stderr, poll to completion-or-timeout, kill on deadline), the `RALPHY_DONE_EXIT` / -`RALPHY_BLOCKED_EXIT` sentinel parser, and skill/plugin materialization. It is +`RALPHY_BLOCKED_EXIT` sentinel parser, and skill/plugin materialization — +including the `.agents/skills` exposure dance (link-or-copy, symlink-safe +removal, merged per-entry `.gitignore`) that Codex and Copilot both drive. It is the deliberate counterpart of **Adapter**: where an adapter holds what is vendor-specific, adapter support holds what is common. It owns **no** completion protocol and produces **no** `Outcome` — it hands back raw captured output and -each adapter still classifies it (the seam ADR-0004 protects). Lives in +each adapter still classifies it (the seam ADR-0002 protects). Lives in `ralphy-adapter-support`; depended on by the vendor adapter crates, never by the core. -_Avoid_: shared runner, headless runner (ADR-0004 forbids a shared *Outcome* +_Avoid_: shared runner, headless runner (ADR-0002 forbids a shared *Outcome* runner — this is only the plumbing), utils, helpers. **Run deadline / per-issue budget / idle watchdog**: @@ -164,7 +166,7 @@ beats closing a throttled session) and a `timeout`; a `done` needs only protocol-completion and flake-repair hand-backs legitimately finish with no commit (the plan lives in gitignored `.ralphy/plan.md`). `committed` is a *progress* signal feeding the Claude headless no-commit **streak**, not a gate on **green**. This -*narrows* — does not reopen — ADR-0004: raw→signal extraction (including limit +*narrows* — does not reopen — ADR-0002: raw→signal extraction (including limit trustworthiness and exit normalization) stays per-adapter; only the signal→`Outcome` ordering is shared (ADR-0023). Claude is the reference implementation; the behavior change lands on the Codex and OpenCode adapters. @@ -187,6 +189,20 @@ An **optional adapter capability**, not a core guarantee — a deterministic ada which is a deterministic knob the operator sets, not an auto-judged choice. _Avoid_: model selection (too broad), auto-model. +**Effort**: +The deterministic reasoning-depth knob the operator sets per phase +(`--plan-effort`/`--exec-effort`), on the fixed five-rung ladder +`low | medium | high | xhigh | max` (ADR-0044) — the cross-vendor intersection of +the CLIs that expose one. `low`/`medium`/`high` are the guaranteed-universal core; +`xhigh`/`max` are accepted but clamp down on a model that cannot honour them, so +asking for more never silently delivers less. One word, translated to each +vendor's dialect **inside** the adapter (clamp where the vendor degrades silently, +passthrough where it errors loudly, a documented no-op where there is no effort +axis) — never a raw passthrough. Distinct from **complexity routing** (auto-judged +model choice) and from model selection: effort is *how hard*, not *which model*. +_Avoid_: reasoning level (vendor-specific), variant (that is OpenCode's dialect, +not the Ralphy word). + **Supervised session**: Live human oversight of a *running* agent session — following it and intervening mid-flight, via Remote Control (mobile) or an on-screen terminal (local/Tauri). @@ -406,7 +422,7 @@ An issue's `## Blocked by` section names other issues (`#N`) it depends on. The runner gates on it: if any named blocker is still **open**, the blocked issue is *skipped* this run (not closed, not a stop) and picked up by a later run once the blocker clears. A blocker counts as satisfied when simply **closed** — safe only -because every issue in a run shares one branch (see ADR-0002). +because every issue in a run shares one branch (see ADR-0045). _Avoid_: depends-on, prerequisite, stop-before (that's flow control, not a dependency). **stop-before**: diff --git a/Cargo.lock b/Cargo.lock index 9f128577..cf4200ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -622,10 +622,21 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + [[package]] name = "globset" version = "0.4.18" @@ -1506,9 +1517,15 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "ralphy-adapter-support" -version = "0.1.0-rc13" +version = "0.1.0-rc15" dependencies = [ "anyhow", "include_dir", @@ -1520,7 +1537,7 @@ dependencies = [ [[package]] name = "ralphy-agent-claude" -version = "0.1.0-rc13" +version = "0.1.0-rc15" dependencies = [ "anyhow", "include_dir", @@ -1536,7 +1553,7 @@ dependencies = [ [[package]] name = "ralphy-agent-codex" -version = "0.1.0-rc13" +version = "0.1.0-rc15" dependencies = [ "anyhow", "include_dir", @@ -1548,9 +1565,59 @@ dependencies = [ "tracing", ] +[[package]] +name = "ralphy-agent-copilot" +version = "0.1.0-rc15" +dependencies = [ + "anyhow", + "include_dir", + "ralphy-adapter-support", + "ralphy-core", + "ralphy-usage-scan", + "rusqlite", + "serde", + "serde_json", + "tempfile", + "tracing", + "uuid", +] + +[[package]] +name = "ralphy-agent-cursor" +version = "0.1.0-rc15" +dependencies = [ + "anyhow", + "include_dir", + "ralphy-adapter-support", + "ralphy-core", + "ralphy-proc-util", + "serde", + "serde_json", + "tempfile", + "tracing", + "uuid", +] + +[[package]] +name = "ralphy-agent-gemini" +version = "0.1.0-rc15" +dependencies = [ + "anyhow", + "include_dir", + "ralphy-adapter-support", + "ralphy-core", + "ralphy-proc-util", + "serde", + "serde_json", + "tempfile", + "toml 0.8.23", + "tracing", + "uuid", +] + [[package]] name = "ralphy-agent-kimi" -version = "0.1.0-rc13" +version = "0.1.0-rc15" dependencies = [ "anyhow", "include_dir", @@ -1562,7 +1629,7 @@ dependencies = [ [[package]] name = "ralphy-agent-opencode" -version = "0.1.0-rc13" +version = "0.1.0-rc15" dependencies = [ "anyhow", "include_dir", @@ -1577,7 +1644,7 @@ dependencies = [ [[package]] name = "ralphy-cli" -version = "0.1.0-rc13" +version = "0.1.0-rc15" dependencies = [ "anyhow", "chrono", @@ -1591,6 +1658,9 @@ dependencies = [ "ralphy-adapter-support", "ralphy-agent-claude", "ralphy-agent-codex", + "ralphy-agent-copilot", + "ralphy-agent-cursor", + "ralphy-agent-gemini", "ralphy-agent-kimi", "ralphy-agent-opencode", "ralphy-core", @@ -1611,7 +1681,7 @@ dependencies = [ [[package]] name = "ralphy-core" -version = "0.1.0-rc13" +version = "0.1.0-rc15" dependencies = [ "anyhow", "chrono", @@ -1626,7 +1696,7 @@ dependencies = [ [[package]] name = "ralphy-daemon" -version = "0.1.0-rc13" +version = "0.1.0-rc15" dependencies = [ "anyhow", "axum", @@ -1661,11 +1731,17 @@ dependencies = [ [[package]] name = "ralphy-proc-util" -version = "0.1.0-rc13" +version = "0.1.0-rc15" +dependencies = [ + "anyhow", + "tempfile", + "tracing", + "windows-sys 0.59.0", +] [[package]] name = "ralphy-pty" -version = "0.1.0-rc13" +version = "0.1.0-rc15" dependencies = [ "anyhow", "portable-pty", @@ -2552,6 +2628,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -3035,6 +3122,16 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "xtask" +version = "0.0.0" +dependencies = [ + "anyhow", + "serde", + "serde_json", + "ureq", +] + [[package]] name = "yoke" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index 37020752..7bf18d29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,9 @@ members = [ "crates/ralphy-cli", "crates/ralphy-agent-claude", "crates/ralphy-agent-codex", + "crates/ralphy-agent-copilot", + "crates/ralphy-agent-cursor", + "crates/ralphy-agent-gemini", "crates/ralphy-agent-kimi", "crates/ralphy-agent-opencode", "crates/ralphy-pty", @@ -12,6 +15,7 @@ members = [ "crates/ralphy-proc-util", "crates/ralphy-daemon", "crates/ralphy-usage-scan", + "crates/xtask", ] [workspace.package] @@ -36,6 +40,7 @@ directories = "5" toml = "0.8" rusqlite = { version = "0.31", features = ["bundled"] } ulid = "1" +uuid = { version = "1", features = ["v4"] } hostname = "0.4" tempfile = "3" os_info = { version = "3", default-features = false } @@ -44,6 +49,9 @@ winresource = "0.1" ralphy-core = { path = "crates/ralphy-core" } ralphy-agent-claude = { path = "crates/ralphy-agent-claude" } ralphy-agent-codex = { path = "crates/ralphy-agent-codex" } +ralphy-agent-copilot = { path = "crates/ralphy-agent-copilot" } +ralphy-agent-cursor = { path = "crates/ralphy-agent-cursor" } +ralphy-agent-gemini = { path = "crates/ralphy-agent-gemini" } ralphy-agent-kimi = { path = "crates/ralphy-agent-kimi" } ralphy-agent-opencode = { path = "crates/ralphy-agent-opencode" } ralphy-pty = { path = "crates/ralphy-pty" } diff --git a/README.md b/README.md index 25afdd48..2476e153 100644 --- a/README.md +++ b/README.md @@ -1,424 +1,263 @@ -# Ralphy +# Ralphy 🌙 [![Built with Rust](https://img.shields.io/badge/built_with-Rust-orange?logo=rust)](https://www.rust-lang.org/) -[![Platform: Windows | Linux](https://img.shields.io/badge/platform-Windows_%7C_Linux-0078D6)](#prerequisites) +[![Platform: Windows | Linux | macOS](https://img.shields.io/badge/platform-Windows_%7C_Linux_%7C_macOS-0078D6)](https://github.com/paulocorcino/ralphy/releases) [![License: GPL v3](https://img.shields.io/badge/license-GPLv3-blue)](LICENSE) [![Powered by Claude Code](https://img.shields.io/badge/powered_by-Claude_Code-d97757)](https://claude.com/claude-code) -**Ralphy works your GitHub issue backlog while you sleep — and hands you a branch to review in the morning.** +**Ralphy works through your GitHub issues while you sleep — and hands you a branch to review in the morning. ☕** -You label the issues you trust an agent to handle. Ralphy plans each one, has a coding -agent write the code, commits the work, and closes the issue when it's green. It **never -pushes and never opens a PR** — you review the branch and **merge by hand**. It runs on -your **coding-agent subscription** (Claude, ChatGPT/Codex, or your OpenCode provider — no -API key, so no per-token bill). +You tag the issues you trust a coding agent to handle. Overnight, Ralphy takes them one by +one: it **plans** the work, lets a coding agent **write the code**, **commits** it, and +**closes** the issue once the tests pass. In the morning you skim the branch and merge +what you like. -> **Scope:** Ralphy runs on **Windows and Linux** (both built and tested in CI). It -> drives one coding-agent CLI per run, picked with `--agent`: -> **[Claude Code](https://claude.com/claude-code)** (the default), **Codex**, **Kimi**, -> or **OpenCode**. +Three things worth knowing up front: + +- 🔒 **It never pushes and never opens a PR.** Everything stays on one local branch. *You* + review and *you* merge — Ralphy never touches your remote. +- 💳 **No API key, no per-token bill.** It runs on the **subscription** you already pay for + (Claude, ChatGPT/Codex, and more). +- 💻 **Windows, Linux, and macOS.** ```text -You, before bed: Ralphy, overnight: You, morning: -┌──────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐ -│ label issues you trust │ ──────▶ │ plan → code → commit │ ───▶ │ review the branch, │ -│ an agent to handle │ │ → close, issue by issue│ │ merge what you like │ -└──────────────────────────┘ └────────────────────────┘ └────────────────────────┘ + 🌆 You, before bed 🌙 Ralphy, overnight 🌅 You, in the morning +┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐ +│ tag the issues you │ ──▶ │ plan → code → commit │ ──▶ │ review the branch, │ +│ trust an agent to do │ │ → close, one by one │ │ merge what you like │ +└────────────────────────┘ └────────────────────────┘ └────────────────────────┘ ``` --- -## Quick start +## 🤔 What is Ralphy? -```powershell -# Windows (PowerShell) -# 1) Try one issue, plan only — no code changes, no commits. Inspect .ralphy/plan.md. -ralphy run --repo C:\Dev\foo --only-issue 13 --dry-run +Think of Ralphy as a **tireless junior teammate** who picks up small, well-described tasks +from your issue tracker and works them while you're away — carefully, one at a time, and +always leaving the final say to you. -# 2) Run that one issue for real. Commits land on a fresh afk/run- branch. -ralphy run --repo C:\Dev\foo --only-issue 13 +It doesn't replace you. It does the *legwork*: reading the codebase, planning a change, +writing it, running the tests, and closing the ticket when everything's green. What it +delivers is a branch full of finished work for you to review — never a surprise on your +main branch. -# 3) The overnight run: the whole queue, ascending order, with an 8-hour budget. -ralphy run --repo C:\Dev\foo --deadline-hours 8 +## 🔁 What is the "Ralph loop"? -# 4) Run an explicit set of issues, in the exact order given, ignoring queue -# labels and dependency ordering. Drains the list as a sequence. -ralphy run --repo C:\Dev\foo --issues 5,3,9 -``` +The idea behind Ralphy is a simple, repeating loop: + +> **plan → execute → commit → verify → repeat** + +Point an AI coding agent at a task, let it plan and do the work, commit the result, check +that it actually passes — then move to the next task and do it all again. Run that loop +unattended over a whole backlog and you wake up to a pile of done work. + +That pattern is [Geoffrey Huntley](https://ghuntley.com/ralphy/)'s "Ralph" technique. +Ralphy is a careful, batteries-included implementation of it: a single binary that runs the +loop over your **real GitHub issues**, with guardrails so it's safe to leave running while +you sleep. + +--- + +## 🛠️ Set up Ralphy + +Three steps: get the binary, make sure you've got the basics, and initialize your project. + +### 📦 Step 1 — Get the `ralphy` binary + +Grab the archive for your platform from the +[**Releases page**](https://github.com/paulocorcino/ralphy/releases) — Windows, Linux, or +macOS (Intel & Apple Silicon) — and unzip it anywhere. + +Then let Ralphy put itself on your `PATH` so you can type `ralphy` from any folder: ```bash -# Linux (bash) — same flags, POSIX paths -ralphy run --repo ~/dev/foo --only-issue 13 --dry-run -ralphy run --repo ~/dev/foo --only-issue 13 -ralphy run --repo ~/dev/foo --deadline-hours 8 -ralphy run --repo ~/dev/foo --issues 5,3,9 +./ralphy install ``` -`--repo` defaults to the current directory, so from inside the repo you can just run -`ralphy run --only-issue 13`. Work the same setup up incrementally: `--dry-run` one -issue, then one issue for real, and only then trust the unattended overnight queue. - -New to a repo? [docs/getting-started.md](docs/getting-started.md) walks the whole -onboarding — the guided `ralphy init` command and the manual path — from a fresh -clone to a draining queue. - -`--issues 5,3,9` is the manual override: it works exactly those issues, in the order -listed, fetching each by number regardless of its labels and skipping the dependency -sort — the run drains the list as a sequence. Like `--only-issue`, a `stop-before` -label on a listed issue is ignored; unlike it, human-return labels (ADR-0016) are -still respected. It is mutually exclusive with `--only-issue`. - -## Prerequisites - -- A **clean working tree** in the target repo (Ralphy refuses to start on uncommitted - work) and a reachable base branch (default `origin/main`). -- **`gh`** authenticated — check with `gh auth status`. -- The **agent CLI** for your `--agent` choice, signed in to its subscription (no API key): - - `claude` (default) — Claude Code CLI - - `codex` — signed in with `codex login` (use `--agent codex`) - - `kimi` — signed in with `kimi login` (use `--agent kimi`) - - `opencode` — a provider set up with `opencode auth login` (use `--agent opencode`) -- The Ralphy binary on your `PATH` (`ralphy.exe` on Windows, `ralphy` on Linux) — see - [docs/BUILDING.md](docs/BUILDING.md). -- Whatever build tools the issues themselves need on `PATH` (an issue that builds a - feature needs that feature's deps, or it will time out). - -Ralphy works **in place** on whatever repo you point `--repo` at — no worktree, so your -warm build cache (`target/`, `node_modules`, …) is reused. - -## How it works - -For every queued issue, in ascending number order: - -1. **Plan** — the agent reads the codebase and writes a `.ralphy/plan.md` you can - inspect. On Claude, the plan also picks the execution model (a small model for - mechanical work, a strong one for complex work). Issues labelled `stagedplan` are - planned with the bundled `staged-plan` skill. -2. **Execute** — the agent works the plan and commits each step. On Claude you can - follow along and step in from the Claude **mobile app** (each session is named - `ralphy-`); Codex and OpenCode run quietly in the background. -3. **Verify gate** — before closing, Ralphy itself re-runs the commands the plan listed - under `## Verify` (e.g. `cargo fmt --check`, `cargo test`) over the committed code. The - issue only closes if they pass — "green" stops meaning *the agent said so* and starts - meaning *the runner saw the verification pass on the code you'll merge*. Either way it - posts a comment recording each command and its exit code. See - [Verifying before close](#verifying-before-close). -4. **Close on green** — once the gate passes, Ralphy closes the issue with a comment - pointing at the run branch. You still merge by hand. - -If an issue **doesn't** finish cleanly (blocked, stuck, out of time, or the verify gate -fails), the whole run -**stops** and hands you the branch as it stands — so one bad issue can't burn the rest of -the night. Finished issues stay committed; the stalled one's partial work is left for -you to inspect. - -## Which issues get worked - -An issue is in the queue if it carries **any** queue label. The defaults are -`ready-for-agent` and its shorthand `AFK`: - -| Label | What Ralphy does | -|---|---| -| `ready-for-agent` **or** `AFK` | works it, closes it when green | -| `ready-for-human` / `HITL` | never touched — not in the queue | -| `triage-agent` | evaluated by `ralphy triage`; parks the issue out of the run queue until triaged | -| `stagedplan` | planned with the `staged-plan` skill (still needs a queue label to be picked up) | - -`ralphy triage`'s `escalate` verdict routes accepted-but-human-first issues (a -maintainer owes a decision) to `ready-for-human`, keeping them out of the queue — -distinct from `bounce`, which returns reporter-owed gaps to `needs-info`. - -**Human-return precedence** (ADR-0016): a label that returns an issue to a human — -`ready-for-human`/`HITL`, `needs-info`, `needs-triage`, `wontfix`, or `triage-agent` -— outranks any queue label. A queued issue that also carries one is **skipped with a -visible reason** and the run continues; neither `--only-issue` nor `--issues` -overrides it. - -Two extra controls: - -- **`## Blocked by` in the issue body** — if it names an issue that's still open, Ralphy - **skips** the issue (later ones still run) until the blocker is closed. A `## Blocked by` - inside a `ralphy triage` consolidated-spec comment gates the queue the same way. -- **`stop-before` label** — put it on a queued issue and the run stops **before** working - it; every earlier issue still runs. Remove it and re-run to continue. (Create the - `stop-before` label in your repo first.) - -`--queue-label` (repeatable) replaces the default label set entirely. - -## Choosing an agent - -`--agent` picks the CLI for the whole run (default `claude`): - -| `--agent` | Runs | Notes | -|---|---|---| -| `claude` (default) | Claude Code, live session | Mobile Remote Control, model routing, auto-resume on usage limits | -| `codex` | `codex exec`, headless | Scales effort on one model; stops and reports on a usage limit | -| `kimi` | `kimi --print`, headless | Fixed model (`kimi-code/kimi-for-coding`); stops and reports on a usage limit | -| `opencode` | `opencode run`, headless | Fixed model; set effort with `--exec-variant`; stops and reports on a usage limit | - -All four run on a **subscription, not a metered API key** — Ralphy makes sure your -subscription login stays the one in charge. The same `reviewer` and `staged-plan` skills -ship to every agent automatically, so a run never depends on what's installed on your -machine, and your global skills are left untouched. - -**Split planner and executor.** `--agent` picks the executor; `--plan-agent` (default: -the `--agent` value) picks the planner, so you can plan with one agent and execute with -another. The plan is vendor-neutral markdown, so any planner's plan runs under any -executor. The canonical split is `--agent opencode --plan-agent claude` — Claude plans on -its subscription, OpenCode's coder model executes: - -```powershell -ralphy run --agent opencode --plan-agent claude -``` +*(Prefer to build from source? See [docs/BUILDING.md](docs/BUILDING.md).)* -Usage-limit handling is per-phase: a Claude planner can wait out a plan-time reset while -the OpenCode executor stops on an execute-time limit (an explicit `--stop-on-limit` -forces both phases to stop). - -## Everyday flags - -```powershell -ralphy run --agent codex # use Codex instead of Claude -ralphy run --agent kimi # use Kimi (kimi --print, headless) -ralphy run --agent opencode # use OpenCode -ralphy run --agent opencode --plan-agent claude # Claude plans, OpenCode executes -ralphy run --base-branch feature/x # cut the run branch from another base -ralphy run --branch-mode current # commit onto the current branch (no new branch) -ralphy run --exec-model opus # force the execution model for every issue -ralphy run --exec-variant high # OpenCode effort passthrough -ralphy run --remote-control # opt into mobile Remote Control (Claude, off by default) -ralphy run --no-remote-control # per-run override: force it off even if configured on -ralphy run --queue-label my-label # use your own queue label -ralphy run --no-telegram # mute the Telegram monitor for this run -ralphy run --if-idle # no-op (exit 0) if a run is already active — for schedulers -``` +### ✅ Step 2 — The basics you'll need -Run `ralphy run --help` for the full list (planning model/effort, time budgets, and -more). - -Remote Control is opt-in and off by default (#148): pass `--remote-control` per -run, or persist it with `ralphy config set remote_control true`. - -### Scheduled runs (`--if-idle`) - -Ralphy is *the run, not the cron*: put `ralphy run --if-idle` on a timer (Windows -Task Scheduler, cron, GitHub Actions) and the queue drains on schedule. Every run -holds a presence lock (`.ralphy/run.lock`) for its lifetime; an `--if-idle` -invocation that finds a live run logs `skipped: run in progress since