Skip to content

Repository entry: what is automatic, what is decorative, and where it breaks

forensic finding, 2026-09-10: what converges on its own when a person or an agent enters this repository — executable, shared server, episode, peer, continuation — and what does not; the root causes ranked, the matrix, the dependency graph, and the plan by stage

Rendered from docs/ENTRY_AUDIT.md — the same Markdown GitHub shows.

A forensic finding, measured on 2026-09-10 against origin/master at the merge of PR #147, in the primary checkout and in one linked worktree. It answers one question: when a person or an agent enters this repository, what converges on its own — a built executable, a running shared server, an attached session, a peer on the board, a loaded continuation — and what still waits for somebody to remember a command.

The target it is measured against is the invariant a prompt pack put to this repository (the "automatic control plane" pack, SPEC.md): entering an enabled repository or starting a supported agent session converges, idempotently and within bounded time, to one healthy local server, discoverable surfaces, an attached session, peer presence and loaded continuity, without a manual start or join. Every finding below names the file and line it was read from and the command that shows it again. Nothing here is a count the repository can compute; where a number matters, the command that computes it is given instead.

Vocabulary

The pack speaks its own language. This repository already has words for most of it (CONCEPTS.md), and this document uses the repository's.

the pack saysthis repository sayswhere
control planethe shared server and the local servicesADR 0003, ENVIRONMENT.md
runtime desired / observed statethe lease, and what env probesapps/majordomus-cli/src/lease.rs, src/environment/services.rs
service definition / endpoint descriptora web surface, discovered from its producerADR 0013, src/web/discover.rs
session identityan episode (session) opened by the provider's eventADR 0015, lib/session.sh
agent identity, peer presencea peer on the boardADR 0003, src/peers.rs
work claima scope: the task's, or the one a peer announcedCONCEPTS.md (claim), src/peers.rs
handover descriptora handover, resolved by worktree and branchCONTINUITY.md
provider adaptera lifecycle adapter in lib/capture.sh and the hook shimsADR 0009, ADR 0015
control-plane snapshotRepositoryEnvironment (services) + health.report + peers.list + continuity.statefour capabilities today, one value nowhere

The path from entry to cooperation, as it is

cd <repo>                       direnv → .envrc → bin/majordomus-env → `majordomus env export --banner --bridge`
                                exports MAJORDOMUS_ROOT/SHARE/COMPLETION_BIN, and MAJORDOMUS_URL only if a lease exists
                                one TCP connect to the lease's address, no HTTP, no version, no identity
                                never builds, never starts anything                         .envrc:33, src/environment/shell.rs:116-130

agent starts                    Claude Code fires SessionStart → .claude/hooks/majordomus-session-start
                                → `majordomus capture session --event start` → `session start --if-open keep`
                                → briefing on stdout (episode, task, blockers, handover Next Action)
                                no peer board, no server, no announce                       lib/capture.sh:797-816

MCP client attaches             .mcp.json → bin/majordomus-mcp → `majordomus mcp` → lease::elect
                                first process serves (lease + loopback HTTP), later ones bridge
                                the only thing that starts the server                       src/commands/mcp.rs:135, src/lease.rs:89

peer on the board               attach on initialize, named `p<n>`; announce is a tool call the model may or may not make
                                in memory, gone with the process                             src/peers.rs:268, :315

context loaded                  `majordomus context` when somebody runs it; the briefing carried the handover already

What is genuinely automatic today: the executable is built by the first launcher that needs it (bin/majordomus-cli:36-56); the first MCP client starts the server and every later one attaches (ADR 0003); the provider's start event opens the episode and hands the model the last handover's next action (ADR 0015, ADR 0017); a stale lease of every shape is taken over (project.shared-server-resilience); a replaced executable loses its lease (tests/shared_units.rs:147).

What is not: nothing on entry ensures a server; a shell or a non-MCP agent never gets one; a linked worktree gets a server of its own and never sees the repository's; two sessions in one checkout share one episode; the board forgets everything on a restart and never expires anything; no surface shows the server's own state; the one command every worker is told to run first died on a silent peer.

Root causes, ranked

P0 — the invariant cannot hold while these stand

  1. One server per checkout, not per repository. The lease lives at the checkout root (src/lease.rs:63-65), the root is the nearest .ai/manifest.yaml (src/repository.rs:146-160), every worktree has one, and the identity the probe compares is a digest of that root (src/repository.rs:256-261, src/lease.rs:264). So a linked worktree elects its own server on its own port with its own board, and context's fallback to the primary's lease (lib/context.sh:344-353) reads a board that belongs to another checkout, whose peers carry no worktree field. ADR 0003, CLAUDE.md and docs/MCP.md:50 all say "per repository". The pack's worktree scenario is not failing; it is unreachable. Reproduce: in a linked worktree while the primary serves, bin/majordomus-cli env status --format json | jq '.services[0].availability'.

  2. Entry ensures nothing. .envrc watches the lease and evaluates one export (.envrc:24, :33); bin/majordomus-env never builds (bin/majordomus-env:31-50); the SessionStart shim opens an episode and touches no server (lib/capture.sh:797-816). The server exists only after an MCP client spawns bin/majordomus-mcp. A person's shell, a Codex or Gemini session without a lifecycle adapter, or a Claude session whose MCP handshake failed, all enter a repository that has no server and are told nothing that says so. MAJORDOMUS_URL is simply absent (src/environment/shell.rs:116-130). Reproduce: with no lease, bin/majordomus-env export --shell direnv | grep -c URL.

  3. The episode is one per checkout, keyed on nothing the provider sent. session start --if-open keep returns the open episode whatever --provider-session says (lib/session.sh:157-165); the record has no provider field to write (share/allow/session.txt), so continuity.state.provider is declared and never produced (src/capability/builtin/continuity.rs:157-159, :476); an end event from any session closes the episode for all (lib/capture.sh:860-884). Seven concurrent sessions measured on 2026-09-09 had one record between them. The pack's "session registers automatically" is true for the first session and silently false for every other. Reproduce: pipe two SessionStart payloads with different session_id values through .claude/hooks/majordomus-session-start in a fixture, then majordomus session.

  4. Presence is process memory with no expiry, and the reconnect nudge never arrives. The board dies with the server (src/peers.rs:4-12); in the ordinary majordomus mcp owner path nothing calls reap(), so a dead HTTP peer stays attached: true until the owner leaves (src/shared.rs:27-71 has no reaper; src/commands/serve.rs:71-75 is the only loop that does); announcements never expire (src/peers.rs:127-135); on a bridge failover the reinitialize response — the instructions that say "you have not announced anything" — is discarded (src/mcp/bridge.rs:205-221), so the exact incident src/mcp/protocol.rs:331-343 cites is unfixed on the path it happened on. The journal under way on feature/peer-board-survives-restart (ADR 0034, allocated on the board, unpushed at the time of writing) answers the restart half and none of the rest. Since: the server's own reader reaps expired sessions on every path, and the bridge replays its client's last accepted announcement after a re-attach and carries it onto its own board after a takeover (stage 03, on this branch); expiry on announcements and the episode per provider session remain (stage 04). Reproduce: curl -s "$MAJORDOMUS_URL/api/v1/peers" | jq '.peers[] | {id, attached, last_seen_seconds_ago}' after a bridged client has been killed with SIGKILL.

  5. The server's own state is on disk and on no surface, and "ready" has three definitions. The lease carries pid, url, started_at and the executable's identity (src/lease.rs:281-291) and no capability reads it (grep -rn lease apps/majordomus-cli/src/capability/ is empty). health.report counts peers and says nothing else about the process (src/capability/builtin/health.rs:416-420). Readiness is decided independently by Served::ready (the producer's output exists, src/http/surfaces.rs:152-159), by health.ready (capabilities > 0, health.rs:480-486) and by env's bare TCP connect (src/environment/probe.rs:74-89); the lease probe uses a fourth question, GET / plus the identity digest (src/lease.rs:257-268), and none of them asks the version. There is no status, ensure, stop or explain for the server in the executable (src/cli.rs:31-71), and the shell tool's doctor does not know the lease exists (grep -n mcp lib/doctor.sh). Since: the lease is one type read once and server.status serves it, with serve status/ensure/stop on the executable (stage 02, 03); health.report carries a server check decided by that same reading, so the report names the address, the version and whether what answers is the code on disk, and a stale lease is a finding rather than a silence (stage 09, apps/majordomus-cli/tests/health_server.rs). The readiness answers are named rather than merged: four questions about four subjects, each pointed at its owner in docs/MCP.md ("Four readings of "ready"") and in the module doc where the four meet (src/capability/builtin/health.rs). The shell tool's doctor still does not know the lease exists. Reproduce: bin/majordomus-cli --help | grep -c -E 'status|ensure|stop'; curl -s "$MAJORDOMUS_URL/api/v1/health" | jq '.checks[] | select(.id=="server")'.

P1 — the invariant holds only by luck or only for one session

  1. majordomus context died with nothing said whenever the newest peer on the board had not announced. A bare [ -n "$pscope" ] && was the last command of the listing loop (lib/context.sh:398 before the fix); under set -e its status was the pipeline's, and the command exited 1 with nothing on either stream. The case that covers the section never put a silent peer last (test/cases/106_context_peers.sh). Fixed on fix/context-survives-a-silent-peer, case extended. Reproduce (before the fix): attach a second MCP client that announces nothing, then majordomus context; echo $?.

  2. The bootstrap names commands that do not resolve. .envrc:20 puts only bin/ on the path, so majordomus is the shell tool; AGENTS.md and CLAUDE.md tell every worker to run majordomus worktree, majordomus generate, majordomus mcp and docs/ENVIRONMENT.md:8 says majordomus env, and each exits 2 with a redirection to bin/majordomus-cli (bin/majordomus:87-105, :133-143). Both bootstraps are generated from .ai/repo/policy.yaml, so the fix is one template. Reproduce: majordomus worktree; echo $?.

  3. The election has three races the lease cannot see. take_over removes the file unconditionally after a classification that is already old (src/lease.rs:243-254); a superseded takeover leaves the old process serving on its socket beside the new one (src/lease.rs:227-229, src/http/server.rs:77-86); a server whose lease was taken over still holds HELD and its signal handler unlinks the current server's lease on SIGTERM (src/lease.rs:319, :383-397, read, not reproduced); and the fifteen-second bind grace starts before App::load, so a cold start slower than that makes a waiting peer declare the winner abandoned (src/commands/mcp.rs:169, src/lease.rs:130). Two clients at once are tested (tests/mcp_shared.rs:939); three are not; the join timeout path has no test at all. Reproduce: grep -n JOIN_TIMEOUT apps/majordomus-cli/tests/*.rs (empty).

  4. Two claim systems that never meet. A peer's announced scope lives on the board; a task's scope lives in current.yaml across worktrees; check --overlap reads only the latter (lib/start.sh:84-97), peers.announce writes only the former (src/capability/builtin/peers.rs:117); the one place they cross is the context peers section, which is advisory and self-disabling on six conditions (lib/context.sh:348-370) and cannot tell the reader from the peers. CONCEPTS.md:16 defines "claim" as the first and docs/MCP.md:109 uses it for the second. Reproduce: majordomus check --overlap while a peer has announced your scope.

  5. The automatic path is not what the tests run. Case 90 invokes the launcher itself with --http-port 0 and hand-written frames; no test lets a client read .mcp.json or binds the documented default port (test/cases/90_mcp_shared_server.sh:31-60); .envrc is checked as text (test/cases/100_environment.sh:24-57); the peers section is proved against a python stub (106:38-53); scripts/cockpit-probe:49-84 measures whatever server a client already started; the claim mcp-lease-resilience names a case that proves one sentence of five (docs/CLAIMS.yaml:896, 90:107-114); the use case serve-the-layer-to-ai-clients carries four MCP claims and its scenario starts no server; project.shared-server-resilience names CI jobs test and pages that validate.yml does not have. The crate-level suite is strong on crashes (tests/mcp_shared.rs:645, :695, :734, :910) and is skipped for a change that only selects rust-integration (scripts/rust-check:52). Reproduce: grep -rn "8741" apps/majordomus-cli/tests test/cases.

  6. The Cockpit shows no peers, no sessions beyond this checkout, and hides two of its own areas. peers.list has no page, no nav entry, no area (grep -n peer apps/majordomus-cli/src/cockpit/*.rs); continuity is one worktree, one branch; the navigation returns fewer areas than its own doc comment declares, so /cockpit/executions and /cockpit/quality answer but cannot be reached and are outside the probe gate that crawls the navigation (src/cockpit/nav.rs:7-11 against :85-154, scripts/cockpit-probe:101-105); the page list in tests/cockpit.rs:14-45 and the route table in docs/COCKPIT.md:41-60 are hand-kept and behind. Reproduce: curl -s "$MAJORDOMUS_URL/cockpit" | grep -c '/cockpit/executions'.

  7. The live channel carries executions and nothing else. The envelope is typed and replayable (src/execution/event.rs:31-47, src/http/events.rs:236-270) and the only publishers are the execution store and the handler sink; a peer attaching, a lease changing hands or a health transition is a log line (src/http/mcp.rs:153, :168, :187). The Cockpit polls worktrees and activity (share/cockpit/worktrees.js:4, activity.js:2). Reproduce: curl -s "$MAJORDOMUS_URL/api/v1/executions/protocol" | jq '.events[].type'.

P2 — drift and duplication that will bite the fix

  1. The lease is parsed by hand in four places with no Lease type: src/lease.rs:204-240, :73-78, src/environment/services.rs:120-131, lib/context.sh:356 (jq), .just/serve.just:19 (sed, which breaks the day the file is pretty-printed).
  2. docs/MCP.md:32 says nothing starts the server but a client; serve does (src/commands/serve.rs:53-62) and on a terminal runs forever with no client (:70-76). ADR 0003 says Swagger UI is at /docs; it is at /swagger (src/http/swagger.rs:35). The superseded takeover is documented in a code comment and a unit test and in no document. docs/WEB.md:30-40 omits /events while claiming nothing in its table is typed twice. docs/ENVIRONMENT.md:59 omits --bridge and :74-75 names .just/env.just, which does not exist; just env exists only in the untracked generated bridge. lib/capture.sh:709-711 says the start event writes nothing to stdout; :784-796 says the opposite and is what runs.
  3. Facts written twice: the lease path in eight files; the default port in the claims pages and docs/MCP.md; the executable's path composition in justfile:28-29 and lib/rust_bin.sh:52-66; the share directory in justfile:32 and lib/rust_bin.sh:105; the provider name and its payload keys in two adapter tables (lib/capture.sh:108, :137) and four shims; the build profile default in five files. The list to keep is docs/HARDCODING_LEDGER.yaml; these rows are not in it.
  4. doctor never stops on a finding (lib/doctrine.sh:153-163, lib/common.sh:35) and runs over its own budget (MJ_TIMING=1 majordomus doctor); the unpushed-branch warning is a bare mj_warn outside the doctrine registry (lib/doctor.sh:349).
  5. The workflow fingerprint does not cover the generated bridge (src/environment/resolve.rs:596-615), and .envrc does not watch the executable, so a build after entry changes nothing until the lease does.

The audit matrix

capabilitycanonical sourcestarts / registers itselfCLIHTTPOpenAPIMCPCockpitdocstestsstatus
executable present and currentlib/rust_bin.shbuilt by the first launcher; never on entryENVIRONMENT.mdcases 33, 107working, by design manual on cd
shared serversrc/lease.rs, src/shared.rsby the first MCP client onlymcp, serve/infrastructureinitialize instructionsMCP.mdtests/mcp_shared.rs, case 90partial: per checkout, no status/stop
server state (pid, url, started, executable, version)the lease fileMCP.md:46shared_units.rs:81decorative: on disk only
endpoint discoverysrc/web/discover.rsenv serviceslease-file onlyenv status/, /api/v1/environment, /api/v1/web/surfacesyesyes/cockpit/apiWEB.md, ENVIRONMENT.mdtests/environment.rs, case 100working, with a TCP-connect probe
healthsrc/capability/builtin/health.rs/api/v1/health, /live, /readyyesyes/cockpit/healthCOCKPIT.mdtests/http_serve.rspartial: three readiness definitions, no runtime facts
episode (session)lib/session.shby the provider's start eventsession/api/v1/continuityyesyes/cockpit/continuityCONTINUITY.mdcases 54, 55, 60-63partial: singleton per checkout
peer presencesrc/peers.rsattach on initialize; announce by hand/api/v1/peersyesmajordomus_peersnoneMCP.mdmcp_shared.rs:247, case 106 (stub)partial: memory, no expiry, no page
claimsboard announcements / task scopeannounce by hand; start --scopecheck --overlap/api/v1/peers/announce (refused over HTTP)yesmajordomus_announcenoneMCP.md, CONCEPTS.mdpeers.rs units, case 106legacy-conflicting: two systems
handover / checkpointlib/handover.sh, lib/checkpoint.shderived by end and compaction eventshandover, checkpoint/api/v1/continuityyesyes/cockpit/continuityCONTINUITY.mdcases 25, 54, tests/continuity.rsworking
context loadlib/context.sh, lib/derive.shbriefing on startcontextCONTINUITY.mdcases 23, 106working after the fix; the board is not in the briefing
live eventssrc/execution/event.rs, src/http/events.rsexecutions events/eventsprotocol documentyesexecutions pagesEXECUTIONS.mdtests/executions.rs, case 100working, executions only
provider adapterlib/capture.sh adapter tables, .claude/hooks/*installed by capture installcaptureCONTINUITY.mdcases 29, 54working for one provider; others unsupported

Dependency graph

A  identity: one repository, every server of it ─────┐
   (the git repository named beside the checkout;    │
    the servers of one listed from any of them)      │

B  the server's state as one typed value ──────►  C  ensure: entry converges
   (lease read once by a Lease type; status,         (the start event and the MCP launcher
    version, executable, peers, readiness on         call it; a shell is told, not served;
    every surface)                                    bounded, idempotent, concurrency-safe)

D  episode keyed on the provider's session ◄─────────┤
   (one record per session, ended by its owner)      │

E  presence: expiry, reaper, reconnect nudge,     F  events for peers, lease, health
   claims with a lease (journal from ADR 0034)        on the existing typed channel

G  Cockpit: peers, sessions, server state ◄──────────┘
H  gates: cold start through the configured path, storm, crash, two worktrees, two
   providers, drift; rule + ADR; docs

A must land first: every later step that says "the repository" means the git repository, and today the code means the checkout. ADR 0035 takes A and B: the election stays per checkout, because a server serves the layer of the checkout it started in, and what was missing — that the servers of one repository can be told from the servers of two, and listed from any of them — is answered by the index route and server.status. B before C: ensure reports the state it converged to, so the state must have a type first. D is independent of A–C and blocks the pack's "session registers automatically" on its own. E depends on the journal branch or replaces it, and must not be built twice. F, G, H follow.

Legacy to delete or merge, and owners to preserve

Delete or merge:

  • the four hand parsers of the lease (item 13): done for the executable's three by LeaseFile::read; the two shell readers remain until stage 09;
  • lib/context.sh's own lease reader and board fetch (:344-370), once the executable reports the board and the shell tool asks it — one reader, not two.
  • .just/serve.just:12-20 (mcp-status, open) parsing the lease with sed; the recipes stay and call the status command.
  • the four independent readiness answers (P0 item 5); one remains and the others call it.
  • the environment services probe as a bare TCP connect, once a typed status exists.
  • the announcements-only claim system or the task-scope-only one — not both; the task's scope is the durable, enforced form (.ai/repo/features/coordination.md), so the board should carry it rather than a second one.

Preserve, and build on:

  • the election by one atomic create and a lease of every shape taken over (project.shared-server-resilience), and its suites;
  • the environment snapshot as the one value every surface renders (project.envrc-is-an-adapter); ensure is a consumer of it, not a rival;
  • the provider drawing the episode boundary and the derived briefing (ADR 0015, ADR 0017);
  • executions as the typed event channel (ADR 0033) — peers and the lease publish on it, they do not get a second socket;
  • the capability registry as the only place a surface is declared (ADR 0002, ADR 0004).

Refused, and why

  • A server started on cd. project.envrc-is-an-adapter forbids the entry hook to start anything, and ADR 0003 refuses a daemon: there is no process without a client. A shell is not a client. Entry by a shell reports the state and names the command; entry by an agent (the provider's start event, the MCP launcher) converges, because a client is coming.
  • A PID-only health check. Already refused by tests/shared_units.rs:142-146; kept refused.
  • A second event socket for peers. One typed channel exists; a new scope on it is a protocol addition, not a new transport.
  • Ports in more than one place. src/cli.rs:898 is the only declaration; the documents that restate it are projections to regenerate, not facts to keep.

The plan, mapped to the pack's stages

stageslicebranchlands
01this audit; the context fixfeature/entry-convergence, fix/context-survives-a-silent-peernow
02ADR 0035: a checkout's server is one of the repository's (git_repository_id on the index route, every checkout's server listed by server.status); the lease read once by LeaseDocument; the server's standing — absent, starting, ready, outdated, stale — served like every other capabilityfeature/entry-convergencewith this document
03serve status / serve ensure / serve stop on the executable; the start event calls ensure (session.ensure_server_on_start) and the briefing names the server; the idle life of a server no client owns; the election races (the lease kept alive while loading, conditional take-over, publish refused after a take-over, the lease released on loss); the reaper on every path; concurrent ensure at N≥3, a killed server, a stale lease, a taken portfeature/entry-convergencewith this document
04one episode per provider session; the board reaper on the owner path; expiry on announcements; the reinitialize response reaches the model; claims are the task's scopefeature/session-per-provider, on top of ADR 0034's journal branchparallel to 03
05peers, lease and health on the typed channel; a Cockpit area for peers and the server; the navigation derived from the areas it declares; page lists in tests and docs derivedfeature/entry-surfacesafter 03, 04
06the Codex and Gemini lifecycle adapters (data in share/providers.yaml, not code)feature/lifecycle-adaptersparallel
07rule project.entry-converges with a gate; docs/ENTRY.md; HARDCODING_LEDGER.yaml rows for item 15; the bootstrap template names the launcherfeature/entry-convergencewith 03
08cold start through .mcp.json on the default port as an exclusive case; storm; crash at the shell level; two worktrees, one server; two providers; drift injection into the boardfeature/entry-gatesafter 05
09delete the readers in lib/context.sh and .just/serve.just; the three readiness answers (named, not merged: feature/health-names-the-server, with the server check on health.report); the documents in item 14feature/entry-convergence, feature/health-names-the-serverlast

What this audit did not verify

  • The signal-handler race (item 8, third clause) was read, not reproduced.
  • feature/peer-board-survives-restart was read from its worktree, uncommitted; what lands may differ.
  • Entry timing was measured warm, on one machine, three runs; bench is the instrument for a claim.