35. A checkout's server is one of the repository's, and its state is served like every other fact
Context
ADR 0003 decided one shared server per repository: the first majordomus mcp binds
loopback HTTP beside its stdio session, every later one attaches, and the rendezvous is one
lease file under the checkout's local half. The election it describes has held up — a
lease of every shape is taken over, a client is never turned away, a replaced executable
loses its lease — and it was written before ADR 0021 gave this repository linked worktrees
as the ordinary way to work on a branch.
The audit of repository entry (docs/ENTRY_AUDIT.md, measured 2026-09-10) found what the
two decisions had never been made to agree on. The lease lives at the checkout root, the
root is the nearest .ai/manifest.yaml, every worktree has one, and the identity a probe
compares is a digest of that root. So "one server per repository" was one server per
checkout: a linked worktree elected its own server on its own port with its own peer
board, and no reading of any lease said that two such servers belonged together. A session
in one worktree could not see the peers of another, context's peers section read the
primary's board from a worktree by guessing at the primary's path, and the mandate's
worktree scenario — same repository, distinct worktrees, peers that can tell the scopes
apart — was not failing; it was unreachable.
The same audit found the lease read by hand in four places: the election, the read-only
serving, the environment snapshot, and two shell readers (jq in lib/context.sh, sed
in .just/serve.just), with no type between them. What the lease records — pid, address,
start time, the executable — was on disk and on no surface; the health route counted peers
and said nothing about the process; readiness had three unrelated definitions (a
directory's output exists; the registry is non-empty; a TCP connect succeeded); and none of
them asked the one question a stale server class had already cost a day: is the process
behind this address the code on disk?
Decision
A server serves a checkout, and every checkout of a git repository is one repository's. The election is unchanged: one lease per checkout, at the checkout's local half, one atomic create, every stale shape taken over. What changes is that the servers of one repository can now be told from the servers of two, and listed from any of them:
repository::git_identityasks git which repository a checkout belongs to — the git directory every work tree shares — and digests it. The digest is one value for every checkout of the repository and never a path. The index route (/) answers it beside the checkout's ownrepository_id, withlinked_worktree, so that a reader of any server knows which repository it is one of and whether it is the primary. Where git cannot be asked the field is absent; a repository of the layer does not have to be version controlled, and this decision changes nothing for one that is not.- The checkouts of a repository are read from git's own registry of them
(
git worktree list, the reader ADR 0021 already keeps), never from a file of this tool's. There is no registry of servers: a server is found where its checkout is, through the lease that checkout already has.
The lease is one type, read once. lease::LeaseDocument is what a server writes and
what every reader parses; lease::LeaseFile::read is the one reading, and it never fails:
absent, empty, corrupt and a document are its four answers, and what to do about each is
the reader's decision. The election, serving, the environment's service discovery and the
server's status all read through it. A key a reader does not know is ignored, so a lease
written by a newer executable still reads; a key an older lease lacks takes its default.
The document gains version, the executable's version, beside the executable identity it
already carried; the process that publishes a lease keeps the document it wrote
(lease::held), so that it can say what it wrote down without reading its own file back.
The server's state is a capability. server.status — GET /api/v1/server, the tool
majordomus_server, the resource majordomus://server — answers where this checkout's
server stands, measured against what this executable would serve: absent (no lease),
starting (a lease still binding), ready (answers for this checkout, from the file on
disk, at this version), outdated (answers, but from another version or from a file
replaced since it started) or stale (does not answer, or is not a lease), with the reason;
the lease this process holds when it is the server; and every checkout of the repository,
the primary first, each with its lease, its standing and how many peers its server holds.
The decision is a pure function of the lease file, its age, one probe and this executable's
version, tested branch by branch; the capability reads the files and the servers on every
call and caches nothing, because the leases are written by other processes.
How wide the answer is, is the caller's to ask. Listing every checkout costs a lease
read and a probe each, and a machine with a hundred worktrees registered pays a hundred of
both — which a caller that only wants to know about the checkout it is in should not. The
input carries one field, checkouts: repository, every checkout git registers, and
this, this one alone. this is cheap by construction rather than by filtering — git is
never asked for its registry of work trees, so no other checkout is enumerated, no other
lease is read and no other server is probed — and it spawns no process of its own either:
whether the checkout is the primary comes from the stat the git identity already did, and
its branch is read from its own HEAD. The default is repository, because that is the
answer the capability gave before the field existed and it is a compatibility surface; the
readers that only ever wanted this checkout ask for it by name.
Entry converges, and a server no client owns has a bounded life. serve ensure reads
the lease and probes the server it names, as the election does, and starts one as a
process of its own when none answers — serve --fallback --idle, its log beside the lease
— then waits until it is ready; serve stop ends the one the lease names when it answers
for this checkout; serve status is the command-line projection of server.status. The
provider's start event calls ensure (session.ensure_server_on_start, on by default) and
the briefing names where the server stands, so an agent has a server before its first
attach; the MCP launcher has always converged the same way through the election; a shell
entering the repository is told and not served, because the entry hook may start nothing
(project.envrc-is-an-adapter) and a shell is not a client. A server ensure starts ends
when no peer has been attached for --idle seconds, which keeps ADR 0003's line — no
process without a client — true in time rather than at every instant.
An announcement outlives the server it was made to. The bridge keeps the arguments of its client's last accepted announcement and says them again after a re-attach, or carries them onto its own board after a takeover; the one place that sees both the announcement and the reconnection is the one that repeats it, and no worker is asked to remember.
The election keeps its promises under a slow start. An owner keeps its lease young while the layer loads, so a cold start slower than the bind grace is never taken for an abandoned one; a take-over removes only the file it judged; an owner whose lease was taken over while binding refuses to publish and degrades; a server whose lease is taken over later stops claiming it and ends with its peers; and the server's own reader forgets the sessions that stopped pinging on every path.
Alternatives rejected
- Moving the lease to the primary checkout, one server per git repository. It answers the mandate's words literally and serves the wrong tree: a server serves the layer of the checkout it was started in, and a session on a feature branch attaching to the primary's server would read the trunk's rules, decisions and plan. One process indexing every worktree would be the honest form of that decision; it is a larger change than this slice, and this decision does not foreclose it — a server that serves several checkouts would still be listed here, once per checkout.
- A registry of servers under the primary's local half. A file every server writes into and removes itself from is a second lease with the same failure modes and a new stale state of its own. Git already registers the checkouts; the lease each one has is enough.
- Comparing versions in the election. A lease naming a server of another version is
taken over on
fix/stale-runtime-is-loud, which is that decision's own branch; the status reports the mismatch asoutdatedand the election's behaviour is that branch's to change. - A server started on
cd. The mandate that prompted this asks for it; the entry hook may start nothing, ADR 0003 refuses a process without a client, and a shell is not a client. Entry by an agent converges; entry by a shell reports. - A daemon with a supervisor. Refused by ADR 0003 and unnecessary:
ensureis idempotent and concurrency-safe through the election, and a server that ends when idle needs nobody to watch it. - Killing an outdated server from
ensure. A server that answers is somebody's; the status names itoutdatedwith the remedy,stopends it when asked by name, and a replaced executable loses its lease through the election as before.
Consequences
server.status, the git_repository_id and linked_worktree fields of the index route,
LeaseDocument and its version field become compatibility surfaces. Every worktree
carries its own server as before; what is new is that each one can name the others.
context is the first reader to want the narrow question: it asks about the checkout it
runs in and nothing else, and paying for every checkout of the repository is what made
asking the server dearer than not asking it.
context's peers section and the .just/serve.just recipes still read the lease
themselves; they are the readers ADR 0003's amendment left in shell, and they move to the
executable's answer with the next slice (docs/ENTRY_AUDIT.md, stage 09). That slice
landed the same day; see the amendment below.
The mandate's worktree scenario is now reachable: two worktrees of one repository report
the same git.id, distinct checkout_ids, and each other's servers and peer counts. A peer
still carries no worktree of its own on the board; that is the next decision's, on top of
the board journal ADR 0034 brings.
tests/server_status.rs holds the two-worktree case and the stale-lease case;
tests/shared_units.rs and tests/mcp_shared.rs hold the election as before, through the
same typed reading.
Amendment, 2026-09-18: the two shell readers moved, and a gate holds the count at one
The paragraph above deferred the last two shell readers of the lease to stage 09. They
moved on the day this record was written, in 534e8fb7bc — feat(mcp): the shared server's lease has one reader, and a gate that keeps it — so the deferral was never
reached, and this record has been describing a tree that no longer exists since.
What that slice did, and what holds it today:
lib/context.shasksserve status --repo <root> --format jsonand reads the answer'slease.url;.just/serve.just'sopenrecipe asks the same capability for the address of this checkout's server. Neither opens the file;scripts/cockpit-probe, a third reader the paragraph did not name, moved with them.- The rule is
project.the-lease-is-read-once, the gatescripts/ci/lease-reader-check, and the debt it ratchets is.ai/repo/lease-reader-baseline.txt— empty, so the count is one reader, not one reader plus recorded exceptions. test/cases/110_lease_reader.shdrives the gate against fixtures, planting a reader in each language it could be written in and requiring the refusal to name it.
Measured on 2026-09-18 at master 751eb3a581: the gate reports 0 known reader(s) besides
apps/majordomus-cli/src/lease.rs and exits 0; planting a jq hand-parse in
lib/context.sh makes it exit 10, naming the file, the names it matched and the correction;
removing the probe restores exit 0. Independently of the gate, the only tracked files that
name the lease's file name outside the reader, the tests and the documentation are .envrc,
which asks direnv to watch it and never opens it, and the gate itself.
Nothing about the decision changes. This amendment exists so that the record stops contradicting the tree it decided.