Agent Rally Point
Repo-local coordination room for coding agents sharing one checkout — durable facts, live room state, write-boundary checks, and managed sessions. 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 repo-local coordination room for agents working the same checkout. 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.
flowchart LR
A["whoami / enter / ack<br/>(SessionStart + UserPromptSubmit hooks)"] --> B["next<br/>room-state projection"]
B -->|actionable| C["check before-write<br/>SQLite claim snapshot"]
C --> D[Edit / execute]
D --> E["say <fact><br/>claim · artifact · handoff · decision · blocker"]
E --> F[(".rally/log/*.jsonl<br/>append-only, committed, git merge=union")]
F --> G[(".rally/facts.db<br/>SQLite projection — rebuilt by replay when missing/behind")]
G --> B
H["rallyd (opt-in)<br/>single-writer over Unix socket"] -.owns G when running.-> G
E -->|handoff| I["rally inject"]
J["rally run --backend tmux/cmux/ptyd"] --> K[Managed pane]
I --> K
K -->|receipt fact, not screen text| F
G --> L["rally-ui<br/>localhost dashboard, axum+tokio"]
F -.legacy channel, alpha.-> M["agent-rally-watcher<br/>Python push daemon"]
K -.subscribes to Directives, posts Receipts.-> N["rally-termd<br/>separate Easy Terminal repo"]
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.
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.