| title | pact |
|---|---|
| description | Why pact is built the way it is; the problem multi-agent repositories create, and the four primitives that answer it. |
| audience | everyone |
pact is a small, dependency-light CLI that helps multiple coding agents — Claude Code, Codex, or anything else that can run shell commands — work on the same repository without stepping on each other.
New here: download a binary or cargo install — docs/install.md
— then run pact init in a repo.
Everything below is why pact is built the way it is. The reference material — what each command does and how to use it — lives in docs/, and the evidence behind the design lives in the studies. Some pact commands are for the agents, some are for you; cli.md says which.
Say you're running two or three agents against the same repo at once: one fanning out a refactor, another fixing docs, a third writing tests. Without any coordination between them:
- Agent A starts rewriting
src/api.rsright as Agent B edits the same file for something unrelated. One of them loses work. - Agent B renames a function Agent A was about to call. Agent A finds out the hard way, thirty minutes into a build failure.
- Neither agent has any way to say "I'm working on this" or "heads up, I changed the signature of X" without you personally relaying it.
pact doesn't prevent any of this by force. It gives agents a shared, lightweight vocabulary to avoid it on their own.
flowchart LR
A[Agent A] -->|"lease · msg · watch"| P(pact)
B[Agent B] -->|"lease · msg · watch"| P
P --> G["AGENTS.md<br/>the protocol agents read"]
P --> F[".pact/<br/>leases · watches · event log"]
P --> M[".pact/messages.jsonl<br/>+ read cursors"]
P -.->|"on lease release,<br/>the diff goes to watchers"| M
Every box is a file in the repository — pact needs git and a filesystem and
nothing else. The dotted edge is the only automatic step: releasing a lease
delivers what you changed to whoever subscribed to that path.
An agent can only follow a convention it has been told. pact init writes the
protocol into the files agents already read at the start of a session, so you
never explain it again — and points every other instruction file in the repo
back at that one copy, because two copies drift and only one of them can be
checked for staleness.
The failure this prevents is silent: an agent that never learned the protocol
skips leases and messaging entirely, and looks exactly like a fleet that never
started. That is why pact doctor has an opinion about whether the protocol is
current, reachable, and would survive a clone.
init is also bound by the protocol it writes: it refuses to rewrite an
instruction file that is under a live lease. A tool that tells every agent to
lease what it writes, and then writes through those leases itself, is teaching
that the protocol is optional.
A lease is a claim on a path, not a lock the filesystem enforces. Coding agents fail in ways a mandatory lock cannot prevent — crashing mid-edit, ignoring the tool, editing through a channel pact never sees. A claim that expires on its own or can be stolen degrades to "nothing happened"; a real lock degrades to a stuck repository nobody can unblock.
So the design goal is not exclusion, it is making the check cheap enough that agents actually do it, and making the answer honest enough to act on: who holds this, for how long, and what are they doing.
Agents need to hand off context — a renamed function, a changed contract, a defect in someone else's file. A message is a line in an append-only file pact owns, under the same discipline as its event log.
It was not always. Messages were issues in Beads, the tracker agents already used for tasks, on the reasoning that a coordination tool should not invent a message store. That was wrong in one specific way worth stating, because it is a mistake that looks like good taste: it made the agents' task tracker a runtime dependency of coordination, and when the tracker changed what one of its flags meant, pact's tests broke with no change to pact. A subprocess boundary insulates you from how another tool stores things, never from what its commands mean (the full argument).
Three properties came from watching this fail in the field. A message can be addressed to a path rather than a name, because names belong to processes that exit while the work stays. Its id is derived from its own content, so the retry pact's own advice invites lands on the message it repeats rather than minting a second one. And a sender can see whether a peer read a decision, because one who cannot see it re-sends — that last holds within a shared checkout, which is the only place pact has ever coordinated anything.
The protocol has always asked agents to announce an interface change by hand.
That request was tuned twice, in opposite directions, and overshot both times.
Unrestrained, one fleet run produced 85 messages of which 41 were status pings,
and a real BLOCKER sat unread for 38 minutes inside the noise. Restrained —
"the lease note is the announcement; message only when you need something back"
— the next three runs sent four messages between 28 agents, and the collapse
took the load-bearing ones with it. One of those four is the only reason a
runtime panic did not ship.
The lesson is not that agents are lazy. It is that a voluntary step off the
critical path is bimodal under prose: spam or silence, with no reachable middle.
So pact watch asks for nothing at announce time. You subscribe to a path once,
and the diff is delivered as a side effect of pact lease release — a command
those same runs performed 31 times out of 31. Adherence stops being aspirational
and becomes structural.
There is no daemon and nothing that waits: a subscription is a registry entry, and release does the lookup and the sending before it exits.
No daemon. Every command reads state, maybe changes it, and exits. A daemon is one more thing that crashes, drifts out of sync, or has to be restarted before anyone can work — and pact would then be part of the problem it exists to solve.
No MCP write path, though there is a read-only MCP server. Build it with
--features mcp and an observer that cannot run shell commands — an
orchestrator, a status pane — can ask who holds what, what is unread, and whether
the fleet is still moving. It cannot acquire a lease or send a message, and that
is not a gap to close: a lease is a promise made by a named agent doing the
work, so a claim no process stands behind is worse than no claim at all. The
no-daemon line above still holds — the client spawns it on stdio and ends it by
closing stdin. See docs/mcp.md.
No mandatory locking. See above: advisory degrades safely, mandatory deadlocks at 2am with nothing to ask why.
No config file. Every knob is a flag, an environment variable, or a file
under .pact/. Configuration is a second description of your intent that can
disagree with the first.
No Windows build. The coordination model assumes unix semantics rather than
merely running on unix: a lease claim depends on rename and hard_link
atomicity guarantees POSIX makes and Windows does not, and the protocol pact
writes into your instruction files points agents at sh-based commands. A Windows
binary would compile and then be wrong in the one place correctness is the whole
point, so releases ship four unix targets — statically linked on Linux, because a
coordination tool that needs a matching glibc is one more thing to get right
before anyone can work.
No cross-machine coordination. Everything is files on one filesystem, so
two agents coordinate when they can see each other's .pact/. Several
git worktrees of one repository do share it — they are one repository being
edited from several directories, and isolating their leases would produce
advisory locks that advise nobody. Two clones on two machines do not, and closing
that gap needs a consensus story pact has no business inventing.
No database, and no runtime dependency on anyone else's. Leases are files, history and messages are append-only logs, and identities are derived from those rather than registered. Nothing to migrate, nothing that can be out of date with itself, and nothing to install before two agents can coordinate. The one thing pact reads from another tool is a text file bd has already committed — read-only, best-effort, and a clean pass when it is absent.
The reasoning and the boundaries are in docs/architecture.md.
Almost nothing here was designed on a whiteboard. Four repositories have been built by agent fleets coordinating through pact, and a fifth fleet was pointed at pact itself — every agent required to report, with quoted commands and exit codes, what pact had actually done to it. Every finding became a tracked issue citing evidence rather than an opinion.
Four examples of the shape that takes:
pact lease lsused to lead with remaining TTL. A lease 80 seconds old printed3520s, an operator read that as "long-held", and force-released a live agent's claim. Hence age first, an explicit state, and the holder's note.pact msg inboxused to print every body in full and never showed a sender. Seven messages were ~9KB of an agent's context. Hence one line each.pact msg inbox | head -1panicked and exited 101. The message had already been sent; the agent read the status, concluded failure, and sent again. Hence a closed pipe that changes nothing about the exit code.- 51 of 59 messages in one run were never read, because they were addressed to agents that had already exited. Hence delivery that follows the file.
The habit is the point: if you run agents against your own repo, ask each one what the tooling did to it, and require a quoted command as evidence.
The full evidence, and the design decisions each run forced, is in the studies:
| studies/field-runs.md | the four repositories built on pact — arkanoid, megablast, grimcast, crucible — what each measured and what changed |
| studies/dogfooding.md | building pact with pact: the CLI findings, and the two protocol reversals that overshot in both directions |
| studies/experiments.md | the soak, the fault injector, TLA+ and the property search — what each bounds, and why a green run alone proves nothing |
| install.md | mise use -g github:chussenot/pact@latest, downloading a release or building from source, and what pact does and does not need installed |
| cli.md | every command, flag, exit code and --json shape |
| plan.md | the wave-plan manifest, and why contention is a planning problem rather than a lease one |
| onboarding.md | what pact init writes, to which files, and how to check it |
| leases.md | the lease lifecycle: TTL, grace period, steal vs. expiry, path identity |
| messaging.md | the message store, threading, read state, addressing a path, and the 0.9.0 cutover |
| architecture.md | what is stored where, what is committed, and the non-goals in full |
| mcp.md | the optional read-only MCP server: the six tools, and why it cannot write |
| tui.md | pact ui — the four screens, the drill-in stack, keybindings, and the ui build feature |
| telemetry.md | the optional OpenTelemetry export: what leaves the machine and what never does |
| watch.md | pact watch: subscribing to paths, and why interface notification rides lease release instead of asking |
| audit.md | pact audit: what each check proves, and what it deliberately cannot see |
| fleet-patterns.md | how to run a fleet: the orchestrated-wave topology, and the two rules that make its record trustworthy |
| testing.md | the fleet soak and the fault injector: how to run each, and what they cannot prove |
| development.md | build, test, the CI gates and why each exists, the upstream canary |
| performance.md | what the lease hot path costs, measured — and where the time actually goes |
Those pages answer what and how. For what happened when this was used for real — the four field runs, the dogfooding findings, the synthetic harnesses — see the studies above.