A snapshot: read at commit 707a04c7ce8 on 2026-10-18T00:06:01Z by majordomus 0.29.0. Nothing on this page is read again. See it live
docs/ENVIRONMENT.md
The repository environment — one snapshot, every surface a rendering
What it is
- URI
majordomus://document/docs/ENVIRONMENT.md- Kind
document- Identity
docs/ENVIRONMENT.md- Path
docs/ENVIRONMENT.md- Provenance
- declared
- Media type
text/markdown- As a capability
- document.docs/ENVIRONMENT.md
Front matter
{}Content
# The repository environment — one snapshot, every surface a rendering
What this checkout is right now — the project and its version, the repository and its
layer, version control, the toolchains the repository declares, what the layer holds, the
workflows a person can run here, the provider projections and the local services — is one
typed value, `environment::RepositoryEnvironment`, built by one resolver under
[`apps/majordomus-cli/src/environment/`](../apps/majordomus-cli/src/environment/). Every
surface that says any of those things renders that value: `majordomus env` on the command
line, the HTTP route `/api/v1/environment`, the MCP resource `majordomus://environment`,
and the banner direnv draws on entering the directory. None of them discovers anything of
its own. Behaviour as implemented and tested; where this document and the executable
disagree, the document is wrong and changes in the same commit.
The commands and their executable examples are in the generated reference
([`generated/cli.md`](generated/cli.md), under `majordomus env`); the capabilities, their
routes and their benchmark cases in [`generated/capabilities.md`](generated/capabilities.md),
module `environment`. Neither is restated here.
## Two resolutions, one model
Counting what the layer holds means building the index, which costs seconds; the banner
runs on every `cd`. So a snapshot is resolved in one of two modes, and both produce the
same type:
- **full** reads everything, the index included, and writes the cache. `env status` and
`env explain` resolve in full, because a person is waiting for them.
- **fast** reads only what is cheap — one `git status`, one `just --dump`, a few file reads —
and takes the rest from the cache the last full resolution wrote. `env banner`,
`env export` and `env enter` resolve fast, because direnv runs the last of them on every
entry.
What no cache can supply is reported as unknown, never guessed and never defaulted: every
tier of the snapshot carries its state (`resolved`, `cached` or `unavailable`), and an
unavailable value is absent rather than zero. The two modes differ in what they may read,
never in what they mean; the crate's tests hold them to that.
## Provenance
Every field carries where it came from: the file, command or compile-time constant that
decided it, the resolver that read it, and whether it was read now, taken from the cache
or not resolved at all. `env explain` prints it for one field, a prefix or every field;
the capability `environment.explain` answers the same over HTTP and MCP.
## The cache
The cache lives under `.ai/local/state/environment/`, beside the shared server's lease and
for the same reason: it belongs to this checkout, is never tracked, and a fresh clone
starts without it. It holds counts, recipe names and their descriptions, and toolchain
version strings — nothing read from the process environment, no file contents, no path
outside the repository. Each tier carries its own fingerprint over its own inputs, so
editing the justfile expires the workflows and leaves the counts alone. A cache that
cannot be read is a miss, never an error, and writes are atomic. A served request — HTTP
or MCP — never writes it: the cache is the command line's, and a `GET` with a side effect
on the repository would be a defect.
## The shell entry point
`.envrc` is an adapter and nothing else. It puts `bin/` on the path, watches the lease so
that the environment and the banner are re-evaluated when the shared server comes up or goes
away, and evaluates one call: `bin/majordomus-env enter --shell direnv`. That one process
writes the assignments — `MAJORDOMUS_ROOT`, `MAJORDOMUS_SHARE`, and `MAJORDOMUS_URL` when a
server is running — on standard output, where direnv reads the environment it applies, and
the banner on standard error; it refreshes the workflow bridge when a declaration behind it
moved; and it makes sure this repository's shared server is serving this checkout.
`bin/majordomus-env` finds the executable through `lib/rust_bin.sh` and never builds it: a
missing executable is one line on standard error naming `just build`, and exit 0, because a
non-zero exit on a `cd` makes direnv report that the whole environment failed. An executable
older than its sources gets that same line and no runtime — the adapter sets
`MAJORDOMUS_RUNTIME=off` — because a server started from stale code answers with a tree that
is no longer there.
The server entry starts binds loopback, unless this machine's environment names another
interface in `MAJORDOMUS_HTTP_HOST`. Entry inherits that variable and passes it on; it does
not read it, and `.envrc` does not set it. Because `.envrc.local` is sourced after the one
call above, a value written there arrives too late for the server that call starts: it
belongs in the shell's own startup file
([`MCP.md`](MCP.md#the-machine-names-the-interface)).
The rules that hold this shut are `project.envrc-is-an-adapter`
([`.ai/repo/rules/project/envrc-is-an-adapter.v2.md`](../.ai/repo/rules/project/envrc-is-an-adapter.v2.md)) —
a file a shell evaluates on entering the repository resolves the tool and makes exactly one
call to it, the bootstrap command, and evaluates what that call exports; it reads nothing
about the repository, builds nothing, reaches no network and decides nothing — and
`project.entry-converges`, which says what that one call must and must not do. ADR 0043 is
the decision, and `docs/ENTRY.md` the operator's page.
`MAJORDOMUS_BANNER` chooses `auto`, `full`, `compact` or `off`; `MAJORDOMUS_RUNTIME=off`
enters without bringing the runtime up; `NO_COLOR` makes the output plain, and under `CI`
there is no banner at all. The `env` group of the justfile
(`.just/env.just`) carries the recipes a person runs.
Under the banner, entry draws the episode and compact **preflight** checks without repeating
the repository heading: what is proven about this checkout, as
opposed to what it is. The server and its surfaces, the briefing, the rules, the recorded test runs
and the deployment each get a verdict with its evidence. It is a separate value,
`environment::preflight::Preflight`, and [`PREFLIGHT.md`](PREFLIGHT.md) describes it. The banner
takes one thing from it: a service is drawn as answering only when the preflight verified the
server behind it, never on the strength of a connection alone. The server publishes its URL
only after its request workers start, so the lease-triggered reload can reach it. While it
prepares its routes, its lease remains in the starting state.
## What is canonical, and what is derived
| Fact | Canonical source |
|---|---|
| project name, version, licence, summary | the crate manifest, at compile time |
| repository root, layer sections | `.ai/manifest.yaml`, through the repository model |
| version control | one `git status --porcelain=v2 --branch` |
| toolchains | the manifest that declares each one |
| what the layer holds | the index, through the capability registry |
| workflows | `just --dump --dump-format json` |
| provider projections | the policy's `projections[]` |
| services | the executable's own route constants and the shared server's lease |
Nothing in the module writes outside `.ai/local/`, and nothing in it opens a socket to
anything but the loopback address the lease names — and only when asked to probe.