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.
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:
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:
| Milestone | The bug a green suite missed |
|---|---|
| M1 | A 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. |
| M2 | The 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. |
| M3 | A 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. |
| M4a | The 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. |
| M4b | Silent 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.mdexplaining which Rust idiom replaced which Python pattern and why, plus any reference behavior later milestones must respect. The agent that discovered that the reference'sexclude_nonedoesn'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):
| Variant | rho | tau | pi | tau/rho |
|---|---|---|---|---|
--version, with launcher | 12.2 ms | 2506.2 ms | 2184.3 ms | 204.9× |
--version, direct entry | 6.5 ms | 1970.8 ms | 2176.6 ms | 302.2× |
| print, 0 ms latency | 38.8 ms | 2971.8 ms | 2503.5 ms | 76.7× |
| print, 20 ms/chunk streaming | 438.1 ms | 3123.4 ms | 2747.0 ms | 7.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:
| Dataset | entries | rho | tau | pi |
|---|---|---|---|---|
| linear-100k | 100000 | 2.077 s | 8.876 s | 756 ms |
| deep-branch-100k | 100000 | 921 ms | 3.830 s | — |
| linear-1k | 1000 | 20.8 ms | 46.5 ms | 6.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:
| Deltas | rho ns/delta | tau ns/delta | tau/rho |
|---|---|---|---|
| 100 | 1099 | 105826 | 96.3× |
| 1000 | 1166 | 84544 | 72.5× |
| 10000 | 2160 | 86815 | 40.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:
| turns | rho | tau | pi |
|---|---|---|---|
| 1 | 1.98 MiB | 41.47 MiB | 79.98 MiB |
| 500 | 73.03 MiB | 45.11 MiB | 86.33 MiB |
| 2000 | 1086.58 MiB | 68.88 MiB | 103.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 at81de4f8896a9(the same revision the golden fixtures are extracted from), rho built--releasethroughout. - 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, pinode 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_counterwith 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.