Skip to content

The repository environment

the repository environment: one typed snapshot of what a checkout is — project, layer, version control, toolchains, workflows, provider projections, local services — with a provenance entry for every value; the full and the fast resolution, the cache under `.ai/local/`, and the direnv adapter that renders it without reading the repository itself

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

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/. 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, under majordomus env); the capabilities, their routes and their benchmark cases in 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 and env export resolve fast, because direnv runs 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 banner is re-evaluated when the shared server comes up or goes away, and evaluates one call: bin/majordomus-env export --shell direnv --banner. 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. 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.

The rule that holds this shut is project.envrc-is-an-adapter (.ai/repo/rules/project/envrc-is-an-adapter.v1.md): a file a shell evaluates on entering the repository resolves the tool, evaluates what it exports and asks it to render; it reads nothing about the repository, builds nothing and reaches no network.

MAJORDOMUS_BANNER chooses auto, full, compact or off; 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.

What is canonical, and what is derived

FactCanonical source
project name, version, licence, summarythe crate manifest, at compile time
repository root, layer sections.ai/manifest.yaml, through the repository model
version controlone git status --porcelain=v2 --branch
toolchainsthe manifest that declares each one
what the layer holdsthe index, through the capability registry
workflowsjust --dump --dump-format json
provider projectionsthe policy's projections[]
servicesthe 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.