Agent Rally Point
Communications protocol enabling multi-agent collaboration in a local repo. Works across LLMs and systems — durable facts, no server.
The Problem
Run two coding agents in the same repository and they collide: both edit the same file, one overwrites the other’s commit, neither knows what the other has done or decided. The usual fixes — a server, a shared chat, constant manual narration — are heavyweight for what is really a local coordination problem.
What I Built
Rally is a communications protocol enabling multi-agent collaboration in a local repo, and it works across LLMs and systems. Any host that can run a CLI speaks it — Claude Code, Codex, Cursor, a shell script — because the contract is a typed fact on disk, not an SDK or a vendor API. State lives in the repo (.rally/) as an append-only JSONL ledger with a derived SQLite cache — no daemon, no network. Agents post durable facts (claims, releases, blockers, decisions, handoffs), read current room state on demand, and get next-action guidance. A check before-write step enforces write boundaries so two agents do not stomp the same file.
Architecture
Rally is a Rust workspace (6 crates) built around one idea: an append-only fact log is the source of truth, and every other view — room state, next recommendations, the dashboard — is a disposable projection replayed from that log. There is no server and no scheduler; rally is a CLI that agents (and hooks) invoke synchronously. The full loop — self-locate → room state → boundary check → edit → durable fact → derived projection → optional live delivery into a pane — is described below, including where the log lives, what rebuilds from it, and where the two adjacent daemons (rallyd, rally-termd) sit relative to it.
Models — which, where, why:
Models: none. Rally is a deterministic coordination substrate — next’s recommendation, check before-write’s conflict decision, and the room projection are all rule-based reads over the replayed fact log (claim ownership, blocker status, staleness thresholds), not model calls. No LLM, embedding, or inference step sits anywhere in the CLI, the daemon, or the hooks; the coding agents that call rally bring their own models, but Rally itself never does.
Tools & infra — which, why:
- Rust workspace, 6 crates —
rally-cli(binrally, the only surface most agents touch),rally-protocol(the typed Directive/Receipt/Inbox contract shared with the daemon side, zero deps beyond serde),rallyd(optional single-writer daemon),rally-ui(dashboard), pluscockpitd/cockpit-cli— an adjacent daemon+iOS client for remote Claude/Codex session supervision that lives in the same workspace but is a separate product surface, out of scope for the coordination architecture here. - factstr / factstr-sqlite — the event-sourcing library the log and projection are built on: typed append-only segments plus a derived SQLite cache, chosen so “log is truth, cache is disposable” didn’t need to be hand-rolled.
- SQLite (
.rally/facts.db) — gitignored, rebuilt by replaying the committed JSONL segments whenever it’s missing or behind; never itself the source of truth, which is why losing it loses nothing. - Git —
.rally/log/<engagement>.jsonlis committed with amerge=uniongitattribute, so concurrent appends from sibling worktrees merge without conflict; git is the sync transport, not a Rally-owned network layer, which is why “network transport” stays explicitly out of scope. - bpaf — CLI argument parsing for the
rallybinary. - schemars — generates JSON Schema from the typed fact/command structs, keeping every
--jsoncontract machine-checkable rather than documentation-only. - tmux / cmux / ptyd — the three managed-session backends
rally run/rally injectshell out to for live pane delivery.ptyd(Easy Terminal’s daemon) replaced the legacyherdrbackend; the correspondingrally-termdconsumer that subscribes to Rally’s Directives and posts Receipts back lives in the separate Easy Terminal repo, not here. - axum + tokio —
rally-ui’s localhost dashboard server, read-only, for troubleshooting cross-room state. - Python (
tools/agent-rally-watcher, uv/pyproject, v0.1.1 alpha) — an optional push-based watcher daemon usingwatchfiles(kqueue on macOS, inotify on Linux) that tails the older, pre-.rally/~/.agent-rally-point/apps/<slug>/changes.jsonlchannel and dispatches filtered events (stdout, macOS notify; HTTP POST stubbed) to consumers outside the repo. Legacy-channel-only today — it does not read the current.rally/log/segments. - Claude Code / Codex / Cursor hooks (
hooks/hooks.json,.codex/hooks.json,.cursor/hooks.json) — portable, version-controlledSessionStart/UserPromptSubmit/PreToolUse/Stopwiring shipped in the repo itself, not a developer’s global config, so a fresh clone gets identical coordination behavior on any machine. Self-gates on.rally/absence and fail-opens by default (advisory only);RALLY_HOOK_STRICT=1turns aPreToolUsecollision into a hard deny.
The turn loop
The section below walks the system. This one walks the agent — the nine steps every session takes, in order, and what each one does to the log. Four steps append a fact, three read the record back before advising, and two touch it in neither direction.
| # | Step | Does what to the log | What it is for |
|---|---|---|---|
| 1 | rally whoami | reads · writes nothing | Self-locate: host runtime, repo root, repo id, worktree, build id, cwd. host_runtime.ambiguous means stop and resolve which host you are — never guess. |
| 2 | rally enter --tool <t> | writes presence | Registers the session, returns the mission, the current lead, and whether an ack is still owed. Idempotent per protocol session. |
| 3 | rally ack --tool <t> | writes acknowledgement | Confirms the rules, guardrails, lead, and mission were ingested. An unacknowledged agent counts as one that has not really joined. |
| 4 | rally next --tool <t> --json | reads the room | The wake-intent check. Returns actionable, requires_human, stop_reason, suggested_claims, completion. If actionable is false, do not invent work from room state. |
| 5 | rally say claim --scope <type:id> | writes claim | Reserves the resource: 11 types (workspace, repo, file, dir, branch, commit, port, process, service, task, cross-repo) × 4 access modes. On conflict the append is refused with exit 2 and the message names the holder. |
| 6 | rally check before-write --path <p> | reads · advisory | Asks whether a live peer already claimed this path. Path-based only, so it cannot see a database or a port. Warns by default; --strict exits 4 so a harness aborts the write. |
| 7 | the host edits | — | The actual work. No arrow in either direction. |
| 8 | the host verifies | — | Tests, builds, whatever proves the change. Rally stores the evidence string you record; it does not judge the work. |
| 9 | rally say <kind> | writes any of 16 fact kinds | Records the outcome so a peer, or your own restarted session, reads it back instead of asking a human. A handoff is complete only when the receiver writes its own acknowledgement. |
Steps 7 and 8 are the charter drawn as geometry. Rally has no arrow into or out of the two steps where the work actually happens, because it records and advises — it never gates, grants, schedules, spawns, retries, or executes.
Two consequences follow from the loop’s shape rather than from any policy text. There is no push path, so an agent that never runs next is never woken; the pull is the delivery mechanism. And step 6 only covers files, because the boundary check takes a path. Ports, services, branches, and tasks are defended at claim time in step 5 instead — a real defense, since the competing append fails with exit 2 and names the holder, but only for an agent that claims before acting. A resource nobody claimed is checked by nothing.
Walked step by step, with the captured refusal envelopes: Multi-Agent Coordination.
How it works
- An agent self-locates (
rally whoami --tool <id> --json) — host runtime, room, lead, mission, ack state — thenenters the room andacks the startup rules. For Claude Code/Codex/Cursor, theSessionStartandUserPromptSubmithooks do steps 1-2 automatically; the hook self-gates to a no-op when.rally/isn’t present and never blocks (fail-open). rally next --tool <id> --jsonreturnsactionable,requires_human,stop_reason,suggested_claims, andcompletion— a rule-based read over the current room projection, not a model call.- If actionable and the agent is about to edit a shared path,
rally check before-write --path <file> --strictreads a warm SQLite claim snapshot and warns (or, opt-in, hard-blocks) if a peer holds that file. ThePreToolUsehook fires this automatically onEdit|Write|MultiEdit|NotebookEdit, scoped to edits only so it doesn’t fire on every read or bash call. - The agent executes the edit.
- The agent posts a durable fact —
rally say claim|release|blocker|resolve|decision|handoff|artifact|risk|lesson— which appends one typed JSON event to.rally/log/<engagement>.jsonl. This file is canonical: append-only, committed, and carriesmerge=unionso sibling git worktrees’ concurrent appends merge without conflict. .rally/facts.db, a SQLite projection built withfactstr-sqlite, derives current room state (claims, blockers, decisions, artifacts) by replaying the log segments; it’s gitignored and rebuilt automatically whenever it’s missing or stale — a clone with zero cache reconstructs the same room state from the committed log alone.- If
rallydis running (opt-in, started withrally daemon startfor repos with many concurrent agents), it becomes the single writer tofacts.dbover a Unix socket (.rally/rallyd.sock), so concurrentrallyinvocations stop racing the SQLite file directly. With no daemon running, every command opens the cache directly — same behavior, just without the contention fix. - For live delivery rather than passive room reads,
rally run --backend tmux|cmux|ptydstarts an addressable pane and assigns it a readable id (rally run claude→claude-01);rally say handoff --target <tool>records the durable ask, andrally inject <target> --handoff <event-id>pastes and submits it into that pane.ptydis Easy Terminal’s daemon and is the replacement for the removed legacyherdrbackend; on that side, the separaterally-termdprocess subscribes to Rally’s typed Directives (via the sharedrally-protocolcrate) and posts Receipts back. - Delivery is only considered received once the target agent posts its own fact (a receipt, artifact, or resolve) — an unacknowledged handoff returns
ack_state: "timeout"rather than trusting that text landing in a pane was read. rally-ui(axum+tokio, localhost-only) and the optionalagent-rally-watcherPython daemon (alpha, tails the older~/.agent-rally-point/apps/channel) are read-only consumers of this same fact history, for cross-room troubleshooting and external notification respectively — neither writes back into the coordination loop.
Facts are the source of truth; every other view is derived, so history stays auditable and nothing depends on a long-running process staying up.
Tech stack
Rust workspace (rally-cli, rally-protocol, rallyd, rally-ui; cockpitd/cockpit-cli adjacent, separate product surface), built on factstr/factstr-sqlite for the append-only-log-plus-SQLite-projection pattern, bpaf for CLI parsing, schemars for JSON-Schema-checked --json output, and axum+tokio for the rally-ui dashboard. Managed-session delivery shells out to tmux, cmux, or ptyd (Easy Terminal); the rally-termd consumer on the ptyd side lives in a separate repo. An optional Python watcher (tools/agent-rally-watcher, uv-managed, alpha) tails a legacy pre-.rally/ channel via watchfiles. No model, embedding, or inference call anywhere in the coordination path.
Boundaries
Network transport stays out of scope. Files, Git, rsync, shared folders, A2A, or a future service can move facts; Rally defines what the bytes mean. Durable fact store: .rally/log/<engagement>.jsonl. Derived sqlite cache: .rally/facts.db, rebuildable any time from the log. The store quarantines and rebuilds on corruption and tolerates torn ledger lines — designed for agents that crash mid-write.
Results
⚠️ no benchmark yet — no throughput, latency, or concurrency figures have been measured or published for Rally. What’s documented is architectural: state is append-only JSONL with a derived SQLite cache (rebuildable at any time from the log), the store quarantines and rebuilds on corruption, and it tolerates torn ledger lines from agents that crash mid-write. Those are design guarantees, not measured outcomes.
Install
git clone https://github.com/tyroneross/agent-rally-point.git
cd agent-rally-point
cargo install --path crates/rally-cli
Rust 1.85+. Apache-2.0.