Back to projects
On this page
Active Started May 2026

Agent Rally Point

Communications protocol enabling multi-agent collaboration in a local repo. Works across LLMs and systems — durable facts, no server.

Rust Python SQLite tmux cmux ptyd

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.

1 · The turn loopevery session, in order
whoami / enter / ack
SessionStart + UserPromptSubmit hooks
next
room-state projection
check before-write
SQLite claim snapshot
Edit / execute
the host’s own work
say <fact>
claim · artifact · handoff · decision · blocker
2 · The ledgerlog is truth, cache is disposable
.rally/log/*.jsonl
append-only, committed, git merge=union
.rally/facts.db
SQLite projection — rebuilt by replay when missing or behind
facts.db → next closes the loop back to tier 1. The pull is the delivery mechanism — an agent that never calls next is never woken.
3 · Live deliveryopt-in, for handoffs that shouldn’t wait for a poll
rally inject
pastes a recorded handoff into a pane
rally run --backend
tmux / cmux / ptyd
Managed pane
posts a receipt fact, not screen text, back to the log
rally-termd
separate Easy Terminal repo — subscribes to Directives, posts Receipts
4 · Read-only consumersnever write back into the loop
rallyd
opt-in single-writer over Unix socket — owns facts.db while running
rally-ui
localhost dashboard, axum + tokio
agent-rally-watcher
Python push daemon — legacy channel, alpha

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 cratesrally-cli (bin rally, 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), plus cockpitd/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>.jsonl is committed with a merge=union gitattribute, 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 rally binary.
  • schemars — generates JSON Schema from the typed fact/command structs, keeping every --json contract machine-checkable rather than documentation-only.
  • tmux / cmux / ptyd — the three managed-session backends rally run/rally inject shell out to for live pane delivery. ptyd (Easy Terminal’s daemon) replaced the legacy herdr backend; the corresponding rally-termd consumer that subscribes to Rally’s Directives and posts Receipts back lives in the separate Easy Terminal repo, not here.
  • axum + tokiorally-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 using watchfiles (kqueue on macOS, inotify on Linux) that tails the older, pre-.rally/ ~/.agent-rally-point/apps/<slug>/changes.jsonl channel 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-controlled SessionStart / UserPromptSubmit / PreToolUse / Stop wiring 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=1 turns a PreToolUse collision 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.

#StepDoes what to the logWhat it is for
1rally whoamireads · writes nothingSelf-locate: host runtime, repo root, repo id, worktree, build id, cwd. host_runtime.ambiguous means stop and resolve which host you are — never guess.
2rally enter --tool <t>writes presenceRegisters the session, returns the mission, the current lead, and whether an ack is still owed. Idempotent per protocol session.
3rally ack --tool <t>writes acknowledgementConfirms the rules, guardrails, lead, and mission were ingested. An unacknowledged agent counts as one that has not really joined.
4rally next --tool <t> --jsonreads the roomThe wake-intent check. Returns actionable, requires_human, stop_reason, suggested_claims, completion. If actionable is false, do not invent work from room state.
5rally say claim --scope <type:id>writes claimReserves 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.
6rally check before-write --path <p>reads · advisoryAsks 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.
7the host editsThe actual work. No arrow in either direction.
8the host verifiesTests, builds, whatever proves the change. Rally stores the evidence string you record; it does not judge the work.
9rally say <kind>writes any of 16 fact kindsRecords 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

  1. An agent self-locates (rally whoami --tool <id> --json) — host runtime, room, lead, mission, ack state — then enters the room and acks the startup rules. For Claude Code/Codex/Cursor, the SessionStart and UserPromptSubmit hooks do steps 1-2 automatically; the hook self-gates to a no-op when .rally/ isn’t present and never blocks (fail-open).
  2. rally next --tool <id> --json returns actionable, requires_human, stop_reason, suggested_claims, and completion — a rule-based read over the current room projection, not a model call.
  3. If actionable and the agent is about to edit a shared path, rally check before-write --path <file> --strict reads a warm SQLite claim snapshot and warns (or, opt-in, hard-blocks) if a peer holds that file. The PreToolUse hook fires this automatically on Edit|Write|MultiEdit|NotebookEdit, scoped to edits only so it doesn’t fire on every read or bash call.
  4. The agent executes the edit.
  5. 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 carries merge=union so sibling git worktrees’ concurrent appends merge without conflict.
  6. .rally/facts.db, a SQLite projection built with factstr-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.
  7. If rallyd is running (opt-in, started with rally daemon start for repos with many concurrent agents), it becomes the single writer to facts.db over a Unix socket (.rally/rallyd.sock), so concurrent rally invocations stop racing the SQLite file directly. With no daemon running, every command opens the cache directly — same behavior, just without the contention fix.
  8. For live delivery rather than passive room reads, rally run --backend tmux|cmux|ptyd starts an addressable pane and assigns it a readable id (rally run claudeclaude-01); rally say handoff --target <tool> records the durable ask, and rally inject <target> --handoff <event-id> pastes and submits it into that pane. ptyd is Easy Terminal’s daemon and is the replacement for the removed legacy herdr backend; on that side, the separate rally-termd process subscribes to Rally’s typed Directives (via the shared rally-protocol crate) and posts Receipts back.
  9. 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.
  10. rally-ui (axum+tokio, localhost-only) and the optional agent-rally-watcher Python 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.