← All writing

writing/July 17, 2026/24 min

I Made Claude Build a Rust Coding Agent With a Team of AI Agents

A persistent coordinator that never writes code, one builder agent per milestone, and a merge gate whose adversarial half caught a real shipping-blocking bug in every single milestone that green CI missed. Here is the whole system — plus the benchmark numbers, methodology, and install once rho shipped.

Updated 2026-07-22 — rho shipped v0.1.0. This post now carries the full three-way benchmark numbers, the methodology behind them, and install instructions, added as new sections near the end.

There is a coding agent called Pi — a deliberately minimal, readable harness. Someone ported it to Python and called it tau, because tau = 2π ("twotimespi"). I wanted a Rust port, so I called it rho — ρ, the Greek r, for Rust. In physics ρ is density: the same agent, compiled.

Ascii
π → τ → ρ

The joke is the lineage. The interesting part is that I didn't write rho. A team of AI agents did — ~31k lines of reference Python turned into a byte-for-byte Rust port, milestone by milestone, with me acting mostly as a manager who says "no" a lot. This post is the system that made that work, because the naive version ("point an agent at the repo and wait") produces confident garbage.

Everything here is public: the rho repo, its dev-notes journal, and the pull requests where you can watch the gate catch bugs in real time.

The org chart

The mistake people make with multi-agent setups is treating agents as interchangeable workers on a shared task. They aren't. Context is the scarce resource, and a fresh agent with a tight brief beats a tired agent with a huge one. So rho has an actual hierarchy:

Ascii
COORDINATOR (persistent — Claude Fable 5) • runs the design interview, writes the plan + milestone ladder • dispatches ONE builder agent per milestone • runs the merge gate on every PR • owns the memory files, the task board, and an hourly babysit cron • never writes bulk implementation code │ ├── MILESTONE AGENT M1 (Claude Opus 4.8, own branch, own PR) ├── MILESTONE AGENT M2 └── MILESTONE AGENT Mn └── CLUSTER SUBAGENTS (only for ~6k+ LOC milestones) each in an isolated git worktree, merged back

The coordinator runs on Claude Fable 5 — Anthropic's Mythos-class tier, above Opus — and its entire job is judgment: design, dispatch, review, remember. It writes essentially no implementation code, because the moment it starts typing out a thousand lines of serde, it's spending its most valuable capacity on the cheapest kind of work.

Milestone agents — and every reviewer — run on Claude Opus 4.8: strong, but a fraction of the coordinator's cost. That split is the economics of the whole setup — the expensive model plans, gates, and coordinates but never writes bulk code; the cheaper-but-still-strong model does all the execution. Each milestone agent gets one self-contained brief — scope, the exact reference files, a mechanical definition of done, a git workflow — works on its own branch, opens a PR, and resolves the review-bot comments itself.

When a milestone is too big for one context (rho's session-and-CLI milestone was ~7k LOC), the milestone agent becomes a temporary coordinator: it splits the work into independent modules, gives each its own git worktree on its own branch, and merges them back. That third tier is born and dies inside a single milestone.

How the agents actually talk

An org chart tells you who reports to whom. It doesn't tell you how a message gets from one agent to another, and that plumbing is where multi-agent systems usually quietly break. Here's what's actually going on underneath.

Messages are pushed, not polled. An agent talks to another agent with a SendMessage tool call. Delivery isn't a queue the recipient checks — it's a push straight into the recipient's conversation as an injected turn. The key consequence: if the target agent is idle, the message resumes it. Delivery is the wake-up. No agent sits in a loop asking "any mail for me?" — there's nothing to poll, because arrival and activation are the same event.

Turns end with a heartbeat. When an agent finishes a turn it emits an idle notification back to its coordinator — essentially available or failed, plus a reason. This is the coordinator's liveness signal, and it's how a worker dying becomes visible instead of silent. When a milestone builder hit a usage limit, it didn't vanish into the void; it surfaced as a failed heartbeat carrying "session limit — resets 4:10pm." The coordinator now knows both that the agent is down and exactly when it can come back. (What it does with that — the hourly cron that re-dispatched the milestone one minute after the reset — gets its own section below.)

The task board is the pull side. Messages are for nudges; the source of truth is a shared task board. Agents call TaskCreate / TaskList / TaskUpdate against a per-session task store, and tasks carry blockedBy dependencies. A worker pulls the next available task (dependencies satisfied), marks it in_progress, and updates it when done. State lives on the board, not in anyone's inbox — so an agent that missed a message can still reconstruct what needs doing by reading the board.

Some wakes are on a clock. Alongside message-pushes and board-pulls there's a third trigger: scheduled cron jobs, in two flavors. One is recurring — the babysitter, a task that fires on a cadence with no human and no upstream message, reads the board, and pokes whatever's stalled. The other is a one-shot the coordinator schedules for a specific future moment (the load-bearing example: waking the fleet a minute after a known rate-limit reset). Either way, a timer is just another way to push a turn into an agent.

Crossed messages are normal — plan for them. Because inboxes are asynchronous, a worker's "done, PR is up" and the coordinator's "actually, also handle X" routinely pass each other in flight. Early on this bit me: I'd send a follow-up requirement and get back a report written before it arrived, and it looked like the agent had ignored me. The fixes are unglamorous and essential: make instructions idempotent, re-assert requirements instead of assuming they stuck, and — the load-bearing one — trust but git fetch. When a worker says it's done, the coordinator verifies against the actual branch, PR, and CI state rather than the message. The repo is ground truth; the report is a claim.

The plumbing is just files

There's no message broker here, no orchestration framework, no network service. The whole substrate is a directory tree under ~/.claude plus tool calls that read and write it: teams/ holds the roster and the per-agent inboxes, tasks/ holds the board, projects/<project>/memory/ holds the cross-session memory, agents/ holds reusable role definitions, and scheduled-tasks/ holds the crons. You can cat the entire state of the system at any moment. That's the whole trick — the "framework" is a filesystem.

A reusable role is a markdown file with frontmatter (this is the shape of the adversarial reviewer I dispatch at the merge gate):

---name: adversarial-reviewerdescription: Fresh-eyes reviewer for a milestone PR whose tests are already  green. Hunts error paths, resource lifecycles, and reference-fidelity gaps.tools: Read, Grep, Bashmodel: opus--- You are reviewing a milestone PR whose test suite is ALREADY GREEN...

A team member is one entry in the roster — a name, the model it runs on, and the brief it was launched with (paths, ids, and tmux handles redacted here):

{  "agentId": "m3-providers",  "name": "m3-providers",  "agentType": "general-purpose",  "model": "opus",  "backendType": "tmux",  "subscriptions": [],  "prompt": "You are implementing Milestone 3 of rho — all six provider adapters + a mock SSE server. Work autonomously on branch m3-providers, open a PR, resolve bot comments, report back."}

And a task on the board is about as plain as it looks — note blockedBy, which is what lets a worker know M3 can't start until M2 ("3") has landed:

{  "id": "4",  "subject": "M3: all six provider adapters + mock SSE server",  "status": "in_progress",  "owner": "m3-providers",  "blockedBy": ["3"],  "blocks": []}

Three primitives — an inbox that pushes a turn, a board that holds dependencies, a clock that fires crons — and every coordination pattern in this post is built out of them. Nothing more exotic is required, and the fact that it's all files on disk is why I can debug a stuck fleet by reading a directory.

Agents all the way down

The hierarchy isn't three fixed tiers — it's recursive. Any agent that finds its slice too big for one context does exactly what the coordinator did: it spawns its own team members. On the full-CLI milestone the milestone coordinator fanned out into four worktree-isolated builders; the merge-gate reviewer fanned into six cluster sub-reviewers, one per module; the finisher spun up three research subagents to chase reference behavior in parallel. One of those research agents died mid-run — and its parent caught it the same way a coordinator catches a dead worker: by the missing report. It noticed the gap, absorbed the unfinished work itself, and moved on. The babysitting pattern isn't a top-level feature bolted onto the root; it applies at every level, because every parent is a coordinator to its own children.

Reporting follows the spawn edge, not the org chart. A grandchild reports to whatever spawned it, never to the top. When the full-CLI milestone coordinator fanned out into four cluster builders and the merge-gate reviewer into six sub-reviewers, every one of those results went to its immediate parent — which absorbed them, deduplicated the overlap, and forwarded only the synthesis one level up. The persistent coordinator never saw a single grandchild's message, by design: each level compresses detail so no one context window has to hold the whole tree. At peak the fleet was around ten concurrent agents across three levels, and the top-level conversation still received nothing but one clean report per milestone.

What actually keeps ten agents from trampling each other is a clean split: coordination state is shared; work products are isolated. There is one session-wide task board that every level reads and writes — tasks carry blockedBy dependencies, a grandchild flips its cluster task (A1: provider catalog + config) to completed, and the parent watching the board sees it without a message ever being sent. The work surfaces, by contrast, are strictly private: each agent gets its own git worktree on its own branch, and code only ever moves up the spawn edges, through a review gate. Messages are just signals; the board and the repo are the truth. Shared truth, isolated workspaces, upward-only merges — that triad is why roughly thirty PR-days of work by a shifting fleet never once produced a collision.

Two rules keep the recursion from turning into chaos. Merges only ever flow up, and only through a gate — a child's work reaches the parent's branch by passing the parent's review, never by a lateral shove. And each parent babysits only its own children; nobody reaches across the tree to nurse someone else's stall. The one thing that is global is liveness: heartbeats bubble all the way to the top, so the persistent coordinator sees every agent's idle-or-failed ping, grandchildren included, even though it never sees their reports. That asymmetry earned its keep once — a rate-limit death took out an entire grandchild-level cluster and no parent in that branch survived to report it, and the top caught the fleet death anyway, from the pings alone. Gating and memory repeat identically at every depth; liveness is the one primitive that also reaches up. The tree repeats the rest.

The coordinator is just another agent

It's tempting to picture the coordinator as the one thing that's always running — the process at the top holding the whole build in its head. It isn't. It obeys exactly the same event rules as its workers: it exists only while a turn is executing, it sleeps between turns, and it wakes only when something pushes it — me typing, a worker's report or heartbeat landing in its inbox, or a cron it scheduled for itself firing. Nowhere in the tree, top to bottom, is there a loop that sits and spins asking "anything to do yet?" Nothing polls. The root is as event-driven as the leaves.

Which raises a fair question: if the coordinator doesn't persist between turns, how does it remember to do something later? It leaves a note for a future self who won't remember writing it. Each turn knows only the conversation in front of it; a cron entry is how one turn hands the next a piece of persisted intent plus the condition under which to wake and act on it. This is the same move durable workflow engines — Temporal, Airflow — are built on: don't keep a process alive to wait, because a process that waits is a process that can die holding state. Persist the intent and let a scheduler resurrect the work. The rate-limit recovery was exactly this trick. The failed heartbeats carried the reset time; the coordinator wrote itself a precise playbook — re-dispatch this milestone from this brief — scheduled one minute past the reset; and when the timer fired, a cold note-reader with none of the original context executed it faithfully.

The principle underneath all of it: liveness comes from events; correctness comes from state. No agent, the coordinator included, is trusted to hold anything important in its head between turns. All the truth lives outside the agents — in the repos, the task board, the plan file, the memory files. That's what makes total silence safe. When nothing is happening the whole tree is genuinely paused, not crashed: dormancy costs nothing, and because nothing that matters lives in a live process, nothing is lost by everyone being asleep. Any event at all resumes the work — most importantly me typing — and it resumes by re-reading the externalized state fresh rather than from stale in-context memory. That's the deeper reason the gate re-verifies against the actual branch instead of the report: trust, but git fetch isn't only a rule for crossed messages, it's how a stateless coordinator is meant to relate to the world.

There's an honest, faintly funny consequence of building it this way. These notes age exactly like the sticky notes on a human's desk. The recurring babysit prompt still contains my original wrong guess at one reset time — "3:20" — which a coordinator that learned the true reset hours ago re-reads, dutifully, every single hour. It stays because the rest of that prompt is generic and correct, so nobody has had reason to touch it. The system doesn't second-guess its own notes; it reads them and acts. A stale detail in an otherwise-fine instruction just rides along, harmless, forever.

One last thing the Q&A pinned down: there is exactly one persistent coordinator, and everything that looks like a second one is scaffolding. When a milestone is too big for a single context the milestone agent becomes a coordinator — but only for that milestone, and only until it dissolves. Those temporary coordinators never accumulate into a standing hierarchy. The invariants that keep it from sprawling are strict: nothing reaches main except through the one persistent coordinator's gate, and the two things that must never fork — the task board and the cross-session memory — live only at the top. Sub-coordinators borrow authority for the length of a milestone; they never own the board, and they never own the merge.

Grill me before you write anything

The first thing I did was refuse to let it code. I ran a design interview — the coordinator's job was to grill me until every branch of the decision tree was resolved and written into a table of locked decisions: full byte-level read+write compatibility with tau's session format (not "similar" — a session started in tau has to resume in rho and vice versa), all six providers in one milestone, ratatui for the TUI, WASM extensions last, golden fixtures as the source of truth.

This is the highest-leverage hour in the whole project. A builder agent works from a brief, and a brief built on an unresolved decision produces confidently-wrong code that the gate then has to catch. Grilling deletes whole categories of downstream rework. It's the one place the expensive model earns its cost purely by arguing with me.

Fixtures are the truth, and the code is always wrong

Before porting a single line of behavior, the first milestone did something counterintuitive: it extracted a corpus of golden fixtures from tau's own serialization code, at a pinned git revision. Not hand-written expected JSON — the extraction scripts import tau and call the exact functions tau uses in production, then commit whatever bytes come out.

The policy attached to those fixtures is one sentence, and it decides every argument:

If a golden test diffs, the code is wrong — never the fixture.

A fixture a human typed encodes a human's belief about the reference. A fixture the reference printed is ground truth. This flips the usual debate: nobody argues about what "correct" means, because correct is a file on disk. On top of that sits a bidirectional crosscheck that runs the same session through both implementations with the clocks and id generators frozen identically: the session files come out raw byte-identical — the same bytes on disk, committed as a literal interchange artifact — and rho's file then resumes inside tau and replays to the same state.

That matters for one specific reason: it frees the review to hunt for what fixtures can't catch.

The merge gate, and the five bugs green CI never saw

Every PR passes the same gate before a rebase-merge: the coordinator re-runs the tests, lint, and crosscheck itself (not "CI is green on the PR" — actually re-execute); a fresh adversarial reviewer goes line-by-line against the reference; two public review bots — OpenAI's Codex reviewer and CodeRabbit — get every comment resolved; then a fix round; then merge.

The adversarial reviewer gets a very specific brief, and it's the part I'd copy into any serious agent project:

The test suite is already green. Do not re-verify the happy path. Find what the passing tests cannot show — error paths, resource lifecycles (drops, cancellations, file descriptors, child processes), timing and concurrency semantics, fidelity to the reference on inputs the fixtures don't cover. When you suspect a divergence, prove it: run both implementations and diff.

Here's the empirical case for why this exists. In every milestone, the full suite was green — fixtures matched, ported tests passed — and the review layer still found a real, shipping-blocking bug:

MilestoneThe bug a green suite missed
M1A persisted usage: null made the entire session file refuse to load — an untagged enum tried to deserialize null into a struct and failed the whole line. A real session that ever saved a null would be unopenable.
M2The harness was permanently bricked if a consumer dropped the event stream mid-run: Rust's async streams run no finally on drop, so a "running" flag never reset and every future turn was rejected.
M3A total-request timeout would kill any LLM stream slower than the timeout. The reference applies it per-read; the naive Rust port made it a total deadline — fine in tests, fatal on a real 90-second generation.
M4aThe bash tool hung forever whenever a command spawned a backgrounded child — the child held a write pipe open, so the reader never saw EOF. Every test that did it hung.
M4bSilent data loss on a persist failure: the write path swallowed a storage error and returned a stale count, re-appending an already-durable message instead of aborting the turn.

Notice what none of these are: none is a serialization bug. Fixtures already guard those. Every single one is an error path, lifecycle, or timing bug — exactly the class a green byte-for-byte suite is blind to, and exactly what the adversarial brief points at. Each one is written up in the dev-notes, one phase-N.md per milestone.

There's a coda to this from after this post first shipped. The largest PR of the whole project — the full-CLI milestone — went through the same gate, and the bot-review round on it resolved 18 threads: 4 genuine fixes buried among 13 evidence-based rebuttals (a claim from the bot, then a diff against the reference proving it wrong). The two fixes that paid for the entire ceremony: a security catch — on an entropy failure the OAuth flow would have silently fallen back to deterministic bytes, i.e. a predictable PKCE verifier; it now aborts, matching the Python reference, which inherits Python's os.urandom raising rather than quietly degrading. And a classic lost-wakeup race in a cancellation signal: the waiter registered itself after it checked the flag, so a cancel that fired in the gap between the two was lost forever. That's the honest posture toward automated reviewers — they're wrong often enough that every claim gets verified against the reference before a line is touched, but when they're right, they're right about exactly the things a ported test suite cannot see.

And the gate kept earning its keep past the milestones, into the unglamorous release work it also governs. Pushing rho toward a tagged v0.1.0 surfaced two more bugs of exactly this flavor. A Codex-reviewer catch found the HTTP layer emitting a duplicate Content-Type header.json() inserted one and the ported-from-tau provider header list appended a second — which the ChatGPT Codex backend rejects with a 400 "Unsupported content type", silently breaking every openai-codex chat turn while Anthropic and Google merely tolerated it. And hardening the GitHub Actions release workflow closed a script-injection vector: the attacker-influenceable release tag was being interpolated straight into a shell --tag=… fragment. Neither is a serialization bug; neither is anything a green test suite sees. The gate isn't a milestone-only ceremony — it's the standing posture, all the way through shipping.

An agent died on a rate limit. A cron brought it back.

Autonomous agents fail in boring, mechanical ways, and the ops layer is what keeps a fleet from quietly dying. The coordinator runs an hourly babysit cron: it reads the task board, pings the in-flight agents, and distinguishes an idle heartbeat (working, just quiet) from a real stall (dead or blocked).

The best moment of the whole build was when this paid off. A milestone agent hit a rate limit and died on spawn — the kind of failure that would normally strand the run until I noticed. But the failed heartbeat carried the reset time, so the coordinator did the obvious thing: it scheduled a one-shot cron for a minute past the reset, and when that timer fired it re-dispatched the milestone from its brief. The work resumed with zero loss and no human in the loop. And this wasn't a one-off fluke — it happened twice over the build, the same failure mode and the same clean recovery both times. The cron layer isn't belt-and-suspenders; it is the recovery mechanism.

There's a subtler ops detail too: the coordinator and the agents message asynchronously, so a "still working?" and a "done, PR is up" constantly cross on the wire. The fix is to treat messages as idempotent status and reconcile against the actual git and PR state, never against message order.

Knowledge has to outlive the agent

Every agent in this system is disposable — it does a milestone and it's gone. So the facts it learned have to be written down where the next one will read them. Two mechanisms carry that:

  • Memory files — small notes indexed by a MEMORY.md, holding the locked decisions and the hard-won facts, loaded on demand across sessions.
  • The teaching journal — every milestone writes a dev-notes/phase-N.md explaining which Rust idiom replaced which Python pattern and why, plus any reference behavior later milestones must respect. The agent that discovered that the reference's exclude_none doesn't recurse into free-form JSON is long gone; the fact survives it.

What the rewrite actually bought

The design interview locked byte-level compatibility as the whole point — a session started in tau resumes in rho and vice versa — which turns the founding question into something measurable: with the behavior pinned identical, what did compiling it to Rust actually buy? The final milestone answered it with four benchmark families across three implementations of the same agent — pi (the original, JIT-warmed Node/V8), tau (interpreted CPython), and rho (compiled Rust) — measured on one machine. Where a family has no fair pi counterpart the pi column is and the reason is stated, never quietly dropped.

Cold start + end-to-end print latency — process spawn → exit for one print-mode turn against the same mock provider (hyperfine):

Variantrhotaupitau/rho
--version, with launcher12.2 ms2506.2 ms2184.3 ms204.9×
--version, direct entry6.5 ms1970.8 ms2176.6 ms302.2×
print, 0 ms latency38.8 ms2971.8 ms2503.5 ms76.7×
print, 20 ms/chunk streaming438.1 ms3123.4 ms2747.0 ms7.1×

A native binary that just execs and prints is untouchable — single-digit milliseconds against ~2 seconds of interpreter boot. The surprise is that pi ties tau: Node's JIT buys nothing for startup, because pi's shipped bundle plus model-catalog load makes --version as heavy as tau's CPython import graph. The ladder is rho ≪ pi ≈ tau.

Session replay — parse every JSONL entry and replay it into the runtime message list:

Datasetentriesrhotaupi
linear-100k1000002.077 s8.876 s756 ms
deep-branch-100k100000921 ms3.830 s
linear-1k100020.8 ms46.5 ms6.1 ms

This is the honest row, the one that punctures a tidy "Rust wins everything" story. rho beats tau by ~4× — but pi is the fastest of the three. V8's JIT-compiled JSON.parse beats both Python's pydantic and rho's deliberately cautious #[serde(untagged)] trial-decode — which is the price rho pays for byte-parity: tau writes the type discriminator in the fourth field position, so rho can't use serde's fast internally-tagged path and eats trial-decode CPU to reproduce the exact bytes. A byte-compat design tax that lets a warmed JIT win a hot loop. (pi replays its own session format and does a lighter reconstruction, so only the linear rows are strictly comparable — which is why they're the ones to show.)

SSE canonicalization — the per-token bookkeeping every streamed response pays, feeding N text deltas through the canonical-event accumulator:

Deltasrho ns/deltatau ns/deltatau/rho
100109910582696.3×
100011668454472.5×
1000021608681540.2×

tau deep-copies a pydantic model per event; rho clones one working struct — same protocol, a 40× different constant factor at 10k deltas. There's no pi column, and that's documented, not dropped: pi has no standalone canonicalization stage to isolate — its providers emit canonical events inline and carry the partial message by reference (one mutated object), so there's no equivalent unit of work to time. A reminder that the per-token cost is a data-model choice, not a language one.

Peak RSS over a scripted N-turn session — the most surprising family, so it gets a sweep rather than a single number:

turnsrhotaupi
11.98 MiB41.47 MiB79.98 MiB
50073.03 MiB45.11 MiB86.33 MiB
20001086.58 MiB68.88 MiB103.81 MiB

The baseline is the real win: a statically-linked ~2 MiB process against tau's ~41 MiB (CPython + pydantic/httpx/rich/textual) and pi's ~80 MiB (Node/V8 + module graph) — ~21× against Python, ~40× against Node, and it holds against both. But watch the sweep cross tau's line and end 15× worse at 2000 turns, and read the caveat, because that number is not what it looks like.

The honest verdict, from the full report: rho wins decisively where a native binary wins — cold start (~205×) and baseline footprint (~21×) — and those wins hold against Node as firmly as against Python, because they're native-binary wins, not anti-Python wins. It bought essentially nothing for the latency a human feels against a real LLM. rho is the right tool when the agent is a component in something larger — batch replay, tooling, embedding, fast startup, low memory — and a lateral move when it's a person at a prompt waiting on a model. The real deliverable is that it achieves the former while staying byte-for-byte compatible with the latter.

How the numbers were made

Everything above is reproducible with just bench, which runs every family and regenerates the report. The setup:

  • Machine: Mac16,7 — Apple M4 Pro (14 cores), 48 GiB RAM, Darwin 26.5.1.
  • Pinned revisions: pi at 3da591ab74ab (v0.80.10, the installed binary's own bundled internals — never a rebuild), tau at 81de4f8896a9 (the same revision the golden fixtures are extracted from), rho built --release throughout.
  • Cold start: hyperfine, 15 runs with 3 warmup runs, measuring process spawn → exit; both interpreted agents measured via their launcher (tau uv run, pi fnm shim) and direct entry (tau venv, pi node cli.js) to separate launcher cost from runtime boot.
  • Peak RSS: /usr/bin/time -l.
  • Micro-benches (session replay, SSE canonicalization): Criterion, self-tuned sample counts, reported mean ± σ; the tau timers use time.perf_counter with warmup + measured iterations.
  • Determinism: inputs are the pinned fixtures/ (extracted by tau's own serializer); the mock provider replays a fixed SSE body; no network, no clock, no RNG in the in-process families. Every final number was taken in a serial, quiesced window, one family at a time.

Get it

rho is live at v0.1.0. Three ways in:

# Homebrew (macOS / Linux)brew install ramanshrivastava/tap/rho # crates.io — installs a binary named `rho`cargo install rho-code # GitHub Releases installer (fetches the right prebuilt binary)curl --proto '=https' --tlsv1.2 -LsSf \  https://github.com/ramanshrivastava/rho/releases/download/v0.1.0/rho-code-installer.sh | sh

One naming footnote, because it will bite you otherwise: the crates.io package is rho-code (the bare rho name is squatted), but the installed binary is always rho. Install rho-code, run rho. From there, rho opens the ratatui TUI, rho -p "…" is print mode, and /login signs you in against an existing Codex, Anthropic, or GitHub Copilot subscription — credentials stored in ~/.rho/credentials.json, in tau's exact on-disk format, of course.

The honest costs

This is not free, and it's not always worth it.

The gate is the expensive part and the valuable part. Running a fresh adversarial reviewer on every PR, re-executing the whole test matrix as coordinator, and doing a fix round costs real tokens and real wall-clock — the review often costs more than the implementation. It's only worth it because rho has a hard correctness bar and a reference to diff against. Take away the oracle — the reference implementation, the golden fixtures — and the whole thing collapses: the adversarial review has nothing objective to anchor to and degrades into taste, and you're better off just dispatching one agent and reading its PR.

So my honest boundary: use this when you're porting, rewriting, or reimplementing something against a reference, correctness matters more than speed, and the scope genuinely exceeds one context. Skip it for prototypes, glue code, or anything where "looks right and passes tests" is actually good enough. The ceremony is a tax you only want to pay when the gate can catch something.

It's the same lesson I keep coming back to: the harness is the product. Here the harness is organizational — a coordinator, a gate, a cron, and a rule that fixtures are truth. The models are interchangeable. The system around them is what shipped a byte-compatible Rust agent while I mostly watched.


I packaged this whole method as a reusable agent skill — milestone-orchestration, with the milestone brief, the merge-gate checklist, the adversarial-review prompt, and the babysit-cron recipe. The rho repo is the worked example.