What a claim protects, and where it stops
Coding agents sharing one checkout coordinate through a single ledger in the repo — presence, claims, handoffs, and conflicts, with no server between them. Every diagram here is drawn from Agent Rally Point, a shipped implementation, so the nine commands below are the ones two agents actually run.
Every agent shares who it is, what it is working on, and its status on one ledger per repo. Claims serialize conflicting writes — they do not make two agents editing one file at the same moment safe.
The turn loop
Nine steps, one shared record. Rally writes a fact at the points where another agent's behavior should change, and reads the log back before it advises. The two steps in the middle — the actual work — are the host's, and Rally never enters them. Pick a step below the diagram to inspect it.
Self-locate before acting. Returns the host runtime, repo root, repo id, worktree, build id, and cwd.
- Writes
- nothing
- Reads
- room state, presence roster
- Watch for
- host_runtime.ambiguous — stop and resolve which host you are before acting; never guess
Registers this session in the room and returns the mission, the current lead, and whether an acknowledgement is still owed.
- Writes
- presence
- Idempotent
- per protocol session — an existing presence for the same tool and from_session_id is a no-op
- Watch for
- duplicate-active-squad-id — re-enter with a distinct agent id rather than reusing the tool id
Confirms you ingested the rules, guardrails, lead, and mission. The coordination mandate treats an unacknowledged agent as one that has not really joined.
- Writes
- acknowledgement
- Reads
- doctrine, mission, lead seat
- Order
- after enter, before doing work
Asks what to do. This is the wake-intent check — there is no push path, so an agent that never pulls is never woken.
- Returns
- actionable · requires_human · stop_reason · suggested_claims · suggested_commands · completion
- Rule
- if actionable is false, do not invent work from room state
- Caveat
- it recommends one action, so an empty answer is not proof the room is empty — read `rally room`
Reserves a resource before touching it. A claim covers far more than a file: eleven resource types across four access modes.
- Writes
- claim
- Types
- workspace · repo · file · dir · branch · commit · port · process · service · task · cross-repo
- Modes
- exclusive · shared_read · advisory · namespace
- On conflict
- the append is refused with exit 2 and the message names the holder
Asks whether another live agent already claimed this path. Path-based only — it builds a file: scope, so it cannot see a database or a port.
- Writes
- nothing (may record an audit fact)
- --strict
- exits 4 on a stop finding, so a harness aborts the write
- Default
- warns and lets the edit through; every hook exits 0 even when Rally is broken
The actual work. Rally has no arrow into or out of this box, and that is the charter, not an omission.
- Writes
- nothing to the ledger
- Owner
- the coding host
- Charter
- Rally records and advises; it never gates, grants, schedules, spawns, retries, or executes
Tests, builds, whatever proves the change. Rally stores the evidence string you choose to record; it does not judge the work.
- Writes
- nothing to the ledger
- Owner
- the coding host
- Next
- record the outcome with `say artifact --evidence`
Records what happened so a peer, or your own restarted session, can read it back instead of asking a human.
- Writes
- any of the 16 fact kinds
- Auto-presence
- a caller with no presence yet commits one first — which is why a refused claim can exit 1 instead of 2
- Handoff rule
- complete only when the receiving agent writes its own acknowledgement
whoami, next, and
check before-write read it back before advising. edit and
verify have no arrow in either direction — that is the charter drawn as
geometry: Rally never gates, spawns, schedules, or executes.
How the parts fit
A host reaches the record two ways: automatically, through committed lifecycle hooks, or explicitly, through the CLI. Both land in one store. Everything a reader sees is derived on demand from the log, so no live server state exists to lose.
Any coding agent that can run a shell command joins the same room. Four hosts get committed hook wiring out of the box; everything else participates through the CLI on equal terms.
- Wired
- Claude Code, Codex, Cursor, Gemini — hooks ship in the repo
- Everyone else
- the rally CLI, which is host-neutral by design
- Why it matters
- the protocol outlives any one vendor's agent
Makes participation the default instead of a discipline. SessionStart and UserPromptSubmit run self-locate, enter, and ack without the agent being told to; PreToolUse fires the boundary check on an edit.
- Fires on
- SessionStart · UserPromptSubmit · PreToolUse · Stop
- Scoped
- PreToolUse covers Edit, Write, MultiEdit, NotebookEdit — not every read or bash call
- Shipped where
- hooks/hooks.json, .codex/hooks.json, .cursor/hooks.json — in the repo, not a developer's global config, so a fresh clone coordinates identically
- Fails open
- self-gates to a no-op when .rally/ is absent, and every hook exits 0 even when Rally is broken. RALLY_HOOK_STRICT=1 turns a collision into a hard deny.
The host-neutral surface. Everything the hooks do automatically, any shell can do by hand — which is what keeps the protocol portable to a host nobody has written hooks for yet.
- Binary
- rally, the only surface most agents touch
- Contract
- every --json output is generated from typed structs via schemars, so it is machine-checkable rather than documentation-only
- Why both paths
- hooks make it effortless for four hosts; the CLI makes it possible for all the rest
Where a claim is accepted or refused. The policy runs as deterministic host code outside any model's control, so an agent cannot waive or recompute its own acceptance rule.
- Owns
- claim conflict detection · lease expiry · liveness · lead seat · policy
- Deterministic
- no model, embedding, or inference call sits anywhere in this path — the decisions are rule-based reads over the replayed log
- Refusal
- an overlapping claim fails the append with exit 2 and names the holder
A contention fix, not a requirement. In repos running many concurrent agents it becomes the single writer to the derived cache, so parallel rally invocations stop racing the SQLite file directly.
- Start it
- rally daemon start — opt-in, per repo
- Talks over
- a Unix socket at .rally/rallyd.sock
- Without it
- every command opens the cache directly. Same behavior, just without the contention fix.
The source of truth, and the only thing here that is not disposable. One typed JSON fact per line, appended and never rewritten, committed alongside the code it describes.
- Shape
- append-only JSONL segments, one fact per line
- Merges
- a merge=union gitattribute, so sibling worktrees' concurrent appends merge without conflict
- Transport
- git. Files, rsync, or a shared folder work equally well — Rally defines what the bytes mean, not how they move.
- Back-compat
- R1 .rally/ledger.jsonl is still replayed for pre-segmentation repos
A cache, and treated like one. It is rebuilt by replaying the committed segments whenever it is missing or behind, which is why corruption is a non-event rather than an incident.
- Built on
- factstr / factstr-sqlite — the append-only-log-plus-projection pattern, not hand-rolled
- Gitignored
- a clone with zero cache reconstructs the same room state from the committed log alone
- On damage
- the store quarantines and rebuilds, and tolerates torn ledger lines — designed for agents that crash mid-write
Live delivery rather than passive room reads. A pane gets an addressable id, and a recorded handoff can be pasted and submitted into it.
- Backends
- tmux · cmux · ptyd (Easy Terminal's daemon, which replaced the removed herdr backend)
- Address it
- rally run claude names the pane claude-01
- Deliver
- rally inject <target> --handoff <event-id>
- Not delivered until
- the target posts its own fact. Text landing in a pane is not evidence it was read — an unacknowledged handoff returns ack_state: timeout.
Decides where a handoff should go. It does not deliver it, hold state, or retry — it is a pure function, which is exactly why it is finished and the daemon above it is not.
- Pure
- no I/O, no ledger writes
- Returns
- a route plan for someone else to execute
The unfinished edge of this architecture. Delivery needs something that owns attempts across time — leases on a try, a retry policy, a fallback when a pane is gone — and that is the piece with state, which is why it is designed rather than shipped.
- Would own
- attempt leases · retry · fallback
- Status
- designed, not built — the dim toggle above hides it
The whole current state — who is present, what is claimed, what is blocked, who holds the lead.
- Derived from
- a replay of the committed log — never stored as state, so it cannot go stale or be lost
One recommended action for one agent, with actionable, requires_human, and stop_reason. It recommends a single action, so an empty answer is not proof the room is empty.
- Derived from
- a replay of the committed log — never stored as state, so it cannot go stale or be lost
What is reserved right now, by whom, and when the lease expires.
- Derived from
- a replay of the committed log — never stored as state, so it cannot go stale or be lost
Recorded risks that nobody has resolved yet.
- Derived from
- a replay of the committed log — never stored as state, so it cannot go stale or be lost
What was chosen and why, so a restarted session does not relitigate a call the room already made.
- Derived from
- a replay of the committed log — never stored as state, so it cannot go stale or be lost
What got produced, carrying the evidence string the agent chose to record.
- Derived from
- a replay of the committed log — never stored as state, so it cannot go stale or be lost
Work items grouped by owner and state.
- Derived from
- a replay of the committed log — never stored as state, so it cannot go stale or be lost
Dependency order across tasks — what unblocks what.
- Derived from
- a replay of the committed log — never stored as state, so it cannot go stale or be lost
What a claim protects
The automatic check is path-based. A non-file resource is protected only at claim time — and only for agents that claim before acting. This is the sharpest edge in the pattern, so it is drawn rather than described.
check before-write takes
--path. Ports, services, branches, and tasks are defended at claim time
instead — a real defense, since the competing append fails with exit 2 and names the
holder, but only for an agent that claims first.
What a refusal looks like
A refused claim has two honest shapes, and which one you get depends on whether the command had already committed something. Every envelope below was captured from a real binary run in a scratch repo — exit codes and ledger deltas included.
rogue ran enter earlier, so its presence is already canonical. Presence is idempotent per protocol session, so this command appends nothing before admission refuses the claim. Nothing committed, so the honest answer is a plain refusal that names the holder.
$ rally say claim --json --tool rogue --path src/lib.rs --subject collide {"error":"claim conflict: lead_agent holds
file:src/lib.rs (claim fact_f1ec_18cbcd672e16cf50),
which overlaps the scope you requested,
file:src/lib.rs",
"exit_code":2,"ok":false,"product":"rally"} (empty — no result envelope) A caller with no prior presence commits its auto-presence first, and only then does admission refuse the claim. Something did commit, so the command has to own up to the write it made rather than exit as if it had made none.
$ rally say claim --json --tool newcomer --path src/lib.rs --subject collide {
"command": "partial_commit",
"ok": false,
"data": {
"committed": true,
"projection_complete": false,
"message": "part of this command committed canonically
before a later required step failed;
do not retry the whole command",
"append_outcomes": [
{ "committed": true,
"fact": { "kind": "presence", "tool": "newcomer", "seq": 12 },
"warnings": [ { "code": "post_commit_work",
"message": "…claim conflict: lead_agent holds
file:src/lib.rs…" } ] }
]
}
} Room-wide breadth is expressible and gated: the lead may take it, a peer may not. The gate fires before conflict detection, so a workspace:* claim can never land and permanently lock every later claim in the room — the defect this control exists to prevent.
$ rally say claim --json --tool rogue --scope 'workspace:*' --subject "room-wide grab" {"error":"claim refused: scope workspace:* is room-wide
and only the lead may hold it. lead_agent holds the
lead seat, not rogue. Claim the specific paths you are
editing, or ask lead_agent to hand off the lead first.",
"exit_code":2,"ok":false,"product":"rally"}