Back to projects
Active Started May 2026

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.

Rust Python SQLite tmux cmux ptyd
Agent Rally Point

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 &lt;fact&gt;<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 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.

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.