Pattern · Case study

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.

The short version

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.

loop while next says actionable .rally/log/<engagement>.jsonl append-only · one fact per line · committed with the code · merge=union canonical presence ack claim artifact · handoff reads room reads wake reads claims
writes a fact reads the record back the host's own work, which Rally never enters
rally whoami reads · writes nothing

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
rally enter --tool <t> writes · presence

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
rally ack --tool <t> writes · ack

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
rally next --tool <t> --json reads the room · writes a wake intent

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`
rally say claim --scope <type:id> writes · claim

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
rally check before-write --path <p> reads · advisory by default

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 host edits Rally never enters this step

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
the host verifies Rally never enters this step

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`
rally say <kind> writes · artifact, handoff, decision, blocker…

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
Every step but two touches the same file. Presence, acknowledgement, claims, and outputs go down into the log; 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.

The delivery rail is where this architecture is still moving.
HOSTSENTRYSTORE RECORDPROJECTIONSDELIVERY Claude Code Codex Cursor Gemini any shell committed lifecycle hooks SessionStart · PreToolUse · UserPromptSubmit · Stop — advisory by default rally CLI explicit, host-neutral rally-cli core — append, project, advise claim conflict detection · lease expiry · liveness · lead seat · policy rallyd optional per-repo single-writer daemon .rally/log/ segments + archive canonical · append-only · committed · zero data loss R1 .rally/ledger.jsonl still replayed for pre-segmentation repos facts.db · cursors.json derived · gitignored corruption is a non-event replay append derived on demand — no live state to lose room next claims risks decisions artifacts board dag managed panes tmux · cmux · ptyd — run / inject / capture rally-router-core pure route planner — no I/O, no ledger writes deliver a handoff rally-routerd proposed — attempt leases, retry, fallback
canonical write path derived or optional proposed, not yet live
projections
Claude Code · Codex · Cursor · Gemini · any shell the agents doing the work

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
committed lifecycle hooks entry · automatic

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.
rally CLI entry · explicit

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
rally-cli core — append, project, advise store · the decision layer

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
rallyd store · optional per-repo daemon

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.
.rally/log/ segments + archive record · canonical

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
facts.db · cursors.json record · derived

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
managed panes delivery · built

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.
rally-router-core delivery · built

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
rally-routerd delivery · proposed, not built

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
rally room projection · computed on demand

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
rally next projection · computed on demand

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
rally claims projection · computed on demand

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
rally risks projection · computed on demand

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
rally decisions projection · computed on demand

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
rally artifacts projection · computed on demand

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
rally board projection · computed on demand

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
rally dag projection · computed on demand

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
The record is the product; everything else is a view of it. Hooks make participation the default rather than a discipline, the CLI keeps any shell a first-class participant, and both converge on one append-only log per repo. The right-hand rail is delivery — getting a handoff into another agent's pane — and it is the unfinished edge: the planner is a pure function today, and the daemon that would own attempts and retries is designed but not built.

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.

FILE EDIT — CHECKED AUTOMATICALLY host edits a file PreToolUse hook check before-write --path builds a file: scope warns, edit proceeds denies only under RALLY_HOOK_STRICT=1 NON-FILE RESOURCE — CHECKED AT CLAIM TIME say claim --scope service:postgres-dev overlap against live claims 11 types × 4 access modes append refused exit 2, names the holder the loser backs off UNCLAIMED RESOURCE — NOTHING CHECKS IT agent resets the dev DB it happens — the other agent's run breaks no hook fires; the check is path-based only no claim, no check
Claiming before acting is the discipline the automatic path cannot enforce. The hook deconflicts files because 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.

exit 2 — clean refusalledger 9 → 9 · unchangedstdout empty

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
stderr
{"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"}
stdout
(empty — no result envelope)
Two shapes, one rule: the exit code reports whether anything committed. Collapsing them into a single expectation is exactly how the repository's own regression test went stale — it asserted the partial commit for a caller that had already entered, and that caller had long since stopped re-committing presence.