Skip to content

Capabilities

the Rust executable's capability model: one definition, and MCP, HTTP, OpenAPI, Swagger UI, the command line and the generated reference derived from it; what is canonical, how to extend it, how it fails

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

How the Rust executable under apps/majordomus-cli/ exposes what it exposes: one canonical declaration per capability, modules that compose capabilities, a root that composes modules, the registry built from them, one executor every transport calls, and the projections (MCP, HTTP, OpenAPI, Swagger UI, the command line, the benchmark targets, the cache behaviour, the generated reference and manifests) that are derived from the registry and define nothing of their own. Behaviour as implemented and tested; where this document and the executable disagree, the document is wrong and changes in the same commit. The decisions are ADR 2 (the registry and the projections), ADR 4 (modules, the executor, benchmarks as evidence) and ADR 5 (one projection plan, named owners, the site as a view of the registry); the rules are project.interfaces-are-projections, project.rust-canonical-declaration, project.rust-benchmark-coverage and project.rust-hot-path.

ONE CANONICAL DECLARATION   capability! { id, kind?, title, description, input, output,
                                          stability, exposure, tags, cache?, benchmark?, handler }

MODULE COMPOSITION          module! { id, title, description, stability, capabilities: [...] }

ROOT COMPOSITION            compose_modules![repository, objects, capabilities, graph, health, peers, perf]

CAPABILITY REGISTRY         + every declarative object of the layer, validated, frozen, fingerprinted

DERIVED PROJECTIONS         MCP · HTTP · OpenAPI → Swagger UI · CLI · the Cockpit
                            · the execution plane and its live channel
                            · benchmark targets and coverage · cache policy · perf counters
                            · docs/generated/* · the website's /registry/ pages

A contributor adding one capability edits one capability! block (with its typed input and output and the input's benchmark cases) and runs majordomus generate. Nothing else.

A contributor adding a command has one more thing to say, and only one: whether it is the projection of a capability (a CliExposure on that capability's declaration) or belongs to the command line alone, in which case cli::LOCAL in src/cli/local.rs carries the reason and majordomus quality report checks it. A command that says neither is a gate failure rather than an operation quietly missing from the API — the rule is project.operation-transport-parity and the working reference is QUALITY.md.

What is canonical, what is derived, what is not authoritative

CANONICAL                                   DERIVED (projections)          NOT AUTHORITATIVE
capability! blocks, one per module file     MCP tools and resources        examples in prose
  apps/majordomus-cli/src/capability/       HTTP routes                    screenshots
  builtin/<module>.rs, composed by          OpenAPI document               the committed snapshots
  module! and compose_modules!              Swagger UI configuration         under docs/generated/
declarative objects of the layer            capabilities list/describe       (caches of the registry)
  .ai/** as sources.yaml maps them          the Cockpit's pages,           a latency number in prose
how each kind is read and validated           navigation, search, palette,   (evidence lives under
  share/kinds.yaml, share/schemas/*.json      runner form and graphs         .ai/local/benchmarks/ and
  .ai/repo/knowledge/kinds.yaml, schemas/   benchmark targets, coverage      .ai/repo/benchmarks/rust/)
the regression policy                       cache behaviour (executor)     the Cockpit's own markup
  .ai/repo/benchmarks/rust/policy.yaml      perf counters                    (a projection, never a
the graph derivations                       docs/generated/*                  place a fact is stated)
  graph::DERIVATIONS, from the registry     share/allow/*.txt (shell tool)
  and the index                             the website's /registry/ pages
flowchart TD
  B[typed executable descriptors<br/>capability/builtin.rs] --> R[CapabilityRegistry]
  D[declarative objects<br/>.ai/** via sources.yaml] --> I[index] --> R
  K[share/kinds.yaml + share/schemas<br/>+ repository additions] --> I
  R --> M[MCP tools and resources]
  R --> H[HTTP routes]
  R --> O[OpenAPI document] --> S[Swagger UI]
  R --> C[capabilities list / describe]
  R --> K2[the Cockpit<br/>pages, navigation, runner, graphs]
  R --> G[docs/generated] --> W[the website's /registry/ pages]
  K --> A[share/allow/*.txt]

A change to one descriptor, one declarative file, one kind or one schema reaches every projection on the next start or the next majordomus generate; nothing is edited twice.

The Cockpit (COCKPIT.md, ADR 12) is the projection a person reads. It is on this list rather than beside it: its pages are laid out from what a capability answered through the same executor every other transport calls, its navigation catalogues are the registry's modules and the index's kinds, its runner's form is generated from the input schema, and its examples are the capability's own benchmark cases. Nothing in it names a capability, a kind, a route or a graph.

The model

A capability is a descriptor with:

fieldmeaning
idthe canonical identity: a namespace, a dot, an opaque local part (repository.info, rule.majordomus.scope-integrity@1, document.docs/CLI.md); unique across both sources
kindquery: executable, read-only, one typed handler; command: executable, one typed handler, changes this process's own memory and nothing else (a peer announcing itself), bound to POST and announced to MCP clients as not read-only; resource: declarative content, read as it is. No kind writes to the repository, and how long a call takes is not a kind
executionwhat running it as an execution means: the effect and whether two may overlap, classified from the kind; and whether asking it to stop achieves anything, which only its handler can say and which .cancellable() on the declaration declares (EXECUTIONS.md)
title, descriptionthe words every projection shows
input, outputcanonical JSON Schemas; for a query, derived from its Rust types; for a resource, the object view
provenancebuiltin with the module, or declarative with the repository-relative path, directory, source class, section and, for a member of a collection file, the member's key path
exposureexplicit, per projection: mcp (tool name and/or resource URI), http (method and path under /api/v1/), cli (the words after majordomus); absent means not exposed there, and nothing infers one
stabilityimplemented, behaviorally_verified, experimental, planned, unsupported; a planned or unsupported capability may be listed and is never executable through any projection

Id grammar: the namespace matches [a-z][a-z0-9_-]*; the local part is non-empty with no whitespace or control character, any other Unicode included; ids are compared as strings, case-sensitively. Declarative ids are <kind>.<identity>, where the identity is what the kind's identity rule produced: id@version for a rule, name for a prompt, the repository-relative path for a kind without identity fields.

The registry

Built at one place per process, from the builtin executables composed explicitly in capability/builtin.rs and from every object of the index. It refuses to build, naming every party, on a duplicate id (Rust with Rust, Rust with declarative), a duplicate MCP tool name or resource URI, a duplicate HTTP route, a duplicate CLI path, a malformed exposure (a route outside /api/v1/, a tool name outside [a-z0-9_]+), a query or a command without a handler, a resource with one, a command with an MCP resource exposure or an HTTP method other than POST, a query exposed as an MCP resource whose input requires anything (a read supplies none), and an executable exposure on a planned or unsupported capability. Errors are collected, not stopped at the first. majordomus capabilities validate runs exactly this and exits 10 with the list.

Modules and composition

A capability lives in the Rust module of its namespace under apps/majordomus-cli/src/capability/builtin/: repository.rs, objects.rs, capabilities.rs, peers.rs, perf.rs. Each file declares its typed inputs and outputs, implements BenchmarkCases for every input type, writes one handler per capability, and ends with module():

pub fn module() -> ModuleDescriptor {
    module! {
        id: "objects", title: "Objects", description: "...", stability: Stability::BehaviorallyVerified,
        capabilities: [
            capability! { id: "objects.list", ... handler: objects_list },
            capability! { id: "objects.search", ..., cache: CachePolicy::Process { max_entries: 64, ttl_seconds: None }, handler: objects_search },
        ],
    }
}

builtin/mod.rs composes the application, and that line is the only root composition:

pub fn modules() -> Vec<ModuleDescriptor> {
    compose_modules![repository, objects, capabilities, peers, perf]
}

The macros are macro_rules! that build plain values; app.rs hands them to the registry builder explicitly. There is no procedural macro, no global registry filled behind the caller's back, no link-time inventory and no build script reading src/. The registry stamps nothing it did not receive: module! stamps its id on each capability, and a capability whose id namespace is not its module (ModuleMismatch), a module composed twice, an invalid module id, a cache policy that keeps nothing, a cached command or a benchmark policy that contradicts the kind refuses the build, naming the id and the provenance. Executables composed without a descriptor (tests, benchmarks) get a module derived from their namespace; declarative objects get their kind. The registry's summary counts modules, required and waived benchmark targets and cached executables, and capabilities validate prints them.

The executor and the cache

Every call goes through one path, whatever asked for it: Context::executeCapabilityExecutor::execute → the handler. The stdio session, /mcp, the HTTP routes, the capabilities commands and the benchmark runners own protocol conversion and nothing else, so instrumentation and caching apply to every transport at once. The executor counts executions, handler invocations, cache hits, misses and evictions in the process-wide perf::COUNTERS, beside the counters of the work that happens once (repository scans, index builds, registry builds, schema generations, MCP projection builds, OpenAPI builds, HTTP router builds) and phase timings on a monotonic clock; perf.counters answers them over every transport, which is how the structural tests prove that hundreds of requests rebuild nothing.

A cache is policy on the descriptor: CachePolicy::Process { max_entries, ttl_seconds }. The key is the canonical id, the input in canonical form (object keys sorted at every level, so a client's key order never matters) and the registry fingerprint, a sha-256 of the index fingerprint (every object's path and content) and of every descriptor, so two repository states never share an entry. Errors are never cached; a command is never cached and the registry refuses a descriptor that asks; the bound evicts the oldest entry first; nothing is persisted. Two capabilities declare it because a measurement said so (objects.search and capabilities.list); every other handler answers from the immutable index in microseconds and a cache there would be a second copy of nothing. The generic tests iterate every cached capability with every case and with generated inputs: uncached, cold and warm agree, a hit runs no handler.

Benchmarks: every operation, a generated denominator

The benchmark projection derives its targets from the registry against a repository: each executable with a required policy, directly and on every transport its exposure declares, once per case its input type provides; plus the transports' own operations, declared once as system targets (a cold majordomus mcp process, initialize, ping, tools/list, resources/list, resources/read; GET /, /openapi.json, /swagger). Coverage is covered / required with the denominator computed, never typed: a required capability whose input type produced no case for this repository is missing, and capabilities validate, bench coverage --check and CI fail on it; a waiver is a typed reason on the descriptor, reported and never counted.

majordomus bench coverage [--format json] [--check]      # covered / required, per transport and in total
majordomus bench [id] [--transport direct|mcp|http|system] [--profile quick|full|ci] [--format json] [--no-write]
majordomus bench --check                                 # against this platform's baseline, under the policy
majordomus bench baseline update [--profile full] [--allow-dirty]

The runners time a target directly through the executor (cold, the cache cleared before every sample, and warm, for a cached capability; handler invocations counted), over a real loopback socket served by the same process with the input bound as the route binds it (query string for GET, JSON body for POST), and through a real majordomus mcp --standalone child on stdio (one process, many samples; a fresh process per sample for the process-cold target). Statistics: samples, min, p50, p90, p95, p99, max, mean, stddev. A run is a document, majordomus/benchmark-result/v1, with the commit, the dirty state, the build profile, the platform and the registry fingerprint, written under .ai/local/benchmarks/; the accepted baseline is one tracked file per platform under .ai/repo/benchmarks/rust/baseline.<os>-<arch>-<build>.json, promoted only by bench baseline update (a dirty tree refuses without --allow-dirty); the regression policy is .ai/repo/benchmarks/rust/policy.yaml (relative thresholds per metric, an absolute floor under which a difference is noise, per metric the sample count under which it does not gate, so that a quick run fails on its median only and a full run on every percentile, and per-target allowances for the targets the machine's load dominates, such as the process-cold spawn). bench --check reports every line, names new targets and stale baseline entries (a renamed capability is never silently matched), and notes when the registry fingerprint moved. A baseline is compared on its own platform only, and it fails a run only on the host that recorded it: the result document carries the host (CPU brand string and logical core count), and a run on another host, such as a hosted CI runner reading a baseline recorded on a developer's machine, prints every line with "reporting only" and exits 0. A CI runner without a committed baseline compares nothing and says so.

Kinds and schemas, read at run time

Which files are declarative objects, and of which kind, is the repository's: .ai/repo/knowledge/sources.yaml maps pathspecs to kinds. How a kind is read is data too, in the tool distribution's share directory (--share, MAJORDOMUS_SHARE, the repository's own share/, or the one beside the executable):

  • share/kinds.yaml — per kind: the format (markdown, yaml, text), whether front matter is required, which JSON Schema the metadata must satisfy, which fields carry identity, title and description, a version field with its supported values, and for a collection file the list that holds the members. declared: names the kinds a Markdown file may declare for itself through its front matter (context today).
  • share/schemas/<name>.schema.json — one JSON Schema (draft 2020-12) per contract; validated with a JSON Schema validator on every read. A key the schema does not allow is the diagnostic unknown_key; any other failed constraint is schema_violation, naming the path and the constraint.

A repository extends both under its knowledge section: .ai/repo/knowledge/kinds.yaml adds kinds, .ai/repo/knowledge/schemas/<name>.schema.json adds schemas. Adding is the only operation: a kind or a schema the distribution already declares is an error naming both files. The shell tool's allow-lists under share/allow/ are generated from the schemas that carry x-majordomus-allow (majordomus generate allow); they are never written by hand.

Projections

projectionderived fromwhere
MCP toolscapabilities with an mcp.tool exposure; inputSchema and outputSchema are the canonical schemas; _meta.majordomus.id carries the id; readOnlyHint follows the kindmajordomus mcp on stdio, and /mcp on the shared server
MCP resourcescapabilities with an mcp.resource exposure; a query with one is read as JSONmajordomus mcp on stdio, and /mcp on the shared server
HTTP routescapabilities with an http exposure; GET binds every top-level input property as a query parameter coerced by its schema type, POST binds the JSON body (a command's binding); errors map to 400 invalid_input, 404 not_found, 422 refused, 500 internal, 405 for another method on a known paththe shared server majordomus mcp starts, and majordomus serve
OpenAPI 3.1the same routes; operationId is the id; the tags are the modules with their descriptions; every example is one of the capability's benchmark cases, by name, evaluated against the repository's index; the responses are the statuses the router answers for the kind (422 for a command only) and default for what the transport adds; a query parameter is never nullable; x-majordomus-id, -kind, -stability, -provenance, -benchmark, -cache, -mcp, -cli carry the rest; info, licence, contact and externalDocs from about.rs and the crate manifest; schemas hoisted into sorted components; the OAS 3.1 base dialectGET /openapi.json, docs/generated/openapi.json, and the site's /docs/api/ and /openapi.json
Swagger UIa shell page that loads /openapi.json; it embeds no specification; its assets come from the pinned swagger-ui-dist on unpkg, the one part that is not offlineGET /swagger (/docs is the documentation)
command linecapabilities list, describe and projections dispatch through the registry's cli exposure; schema and validate are views of the registry, not capabilitiesmajordomus capabilities …
projection closurethe registry's cli exposures against the clap declaration, both ways: every claim answered by a runnable command, and the commands no capability claimscapabilities projections: majordomus_projections, GET /api/v1/capabilities/projections; the projection line of capabilities validate
referencethe index of modules and builtin capabilities, one page per executable module with every capability in full; declarative resources described by rule, listed live; the command line as clap declares itdocs/generated/capabilities.md, docs/generated/modules/<id>.md, docs/generated/cli.md, docs/generated/cli.{json,yaml} (majordomus/cli/v1)
benchmark targetsevery required executable per exposed transport per case, plus the system targets; the coverage talliesmajordomus bench, docs/generated/benchmarks.md and docs/generated/benchmarks.{json,yaml} (majordomus/benchmark-matrix/v1) — one computation, three encodings
registry manifestthe builtin registry as data: modules, descriptors with schemas and the file each was composed in, declarative kinds, system targets; the boundary the site generator reads for its routesdocs/generated/registry.{json,yaml} (majordomus/capability-registry/v1)
artifact manifestthe generation plan itself: every artifact with the document it projects, its encoding, the schema its content satisfies, its source, size and hashdocs/generated/artifacts.{json,yaml,md} (majordomus/generated-artifacts/v1); read back by artifacts.listmajordomus_artifacts, GET /api/v1/artifacts, majordomus://artifacts, /cockpit/artifacts, and the site's /registry/artifacts/
executionsevery executable capability can be started as an execution, watched over the live channel and read back; the descriptor's execution policy decides what a client may offer, and nothing is registered a second timemajordomus run, executions.*, GET /events, /cockpit/executions (EXECUTIONS.md)
perf countersthe executor's and the startup phases' countersperf.counters: majordomus_perf, GET /api/v1/perf
allow-liststhe schemasshare/allow/*.txt, each under a # provenance banner every reader of one skips
site datasetthe registry (fingerprint, counts, every builtin descriptor in full with its source file, every module with its ids), the index (fingerprint, every object without its content), the kinds, the declared provider projections, the command line, the MCP tools and resources, the HTTP routes, the benchmark targets, coverage, policy and accepted baselines; no timestamps of its own, no absolute paths, no git statesite/data/registry/registry.json (majordomus-site-registry/v2) — majordomus generate site; rendered under /registry/ (overview, executable, modules, capabilities, cli, mcp, benchmarks)
provider bootstrapsthe policy's projections[], the profiles and the provider templates (.ai/repo/providers/, else share/providers/); the stamp carries the policy hash and the content hash; byte-identical to the shell tool's updateAGENTS.md, CLAUDE.md, GEMINI.md, … — majordomus generate providers

What every projection says about itself comes from about.rs: the OpenAPI info, the MCP initialize instructions and the HTTP index (GET /) open with the same summary and carry the same paragraphs, so no interface describes the surface in words of its own. The infrastructure routes /, /openapi.json, /swagger and /mcp are the HTTP projection's own and are not capabilities; /mcp is MCP over HTTP (the Streamable HTTP transport's request half, with Mcp-Session-Id sessions) and exists on the shared server only. One shared server serves a repository: the first majordomus mcp or serve binds it, every later majordomus mcp bridges its stdio to it, and the peers see each other through peers.list; the lifecycle is in MCP.md.

Order is a projection too

A collection has one order, and every surface renders the sequence it was handed. The order is apps/majordomus-cli/src/order.rs: a total order over four parts, most significant first — the semantic group (none sorts last), an explicit rank for the collections whose domain declares one (weight on a moment, an audience, an area, a feature), the label compared naturally (digit runs by value, so item-2 precedes item-10; ASCII case folded, so Alpha and alpha stay adjacent), and the canonical identity.

The identity is what makes the order total rather than merely tidy. Two items with the same label must not exchange places because an unrelated item was added, because a different iterator was used, or because another machine enumerated differently.

To join it, implement order::Ordered on the type — beside the type, not beside a renderer — and call order::canonical(). Nothing else is edited: every projection that shows the collection shows the new sequence.

To validate it, run scripts/ci/order-check. Three of its checks are absolute — no case-folded comparator outside order.rs, no sort key that calls render(), no .localeCompare( under scripts/, share/ or site/ — and one is a ratchet over the debt that predates the rule, held as two counts in .ai/repo/order-baseline.txt: sort sites in the crate outside order.rs, and shell sort invocations not pinned with LC_ALL=C. Both may fall and may not rise; a commit that adopts the canonical order lowers the baseline with scripts/ci/order-check --update.

Grouping is the order's first part, and it is derived too. A capability module's area is the areas of the features that name it in modules:, resolved by the catalogue's own weight — lowest first, ties by id. No module declares an area and no file lists the pairs; the Cockpit's sidebar asks the product model. Two features that name one module and share no area disagree about what it is for: the resolution stays deterministic and the disagreement is reported as contested_area by majordomus product validate, to be settled in the feature file rather than by a tiebreak. ADR 0026 records why the parent is derived rather than declared.

To diagnose an unexpected sequence, read the key rather than the output. A collection ordered somewhere other than order.rs has an opinion of its own; a collection whose key ends before its identity has ties, and a tie is where an order looks like a race when there is none. The rule is project.canonical-order; ADR 0025 records the decision.

The one projection that can drift

Every projection in the table above is built by walking the registry. An MCP tool, an HTTP route, an OpenAPI operation, a benchmark target and a reference page cannot exist without a descriptor, and a descriptor cannot fail to produce one: there is nothing to compare, because there is only one declaration.

The command line is different. It is declared a second time, in clap, in apps/majordomus-cli/src/cli.rs, because clap owns parsing, --help, defaults and value sets, and deriving that from the registry would mean reimplementing an argument parser to avoid writing a path twice. CliExposure is therefore not a projection — it is a claim about a declaration that lives somewhere else, and two declarations can disagree.

They did. repository.scope_classify declared cli: ["scope", "classify"] so that the command module could find its id with by_cli. There is no such subcommand: majordomus scope classify parses classify as a path to judge and answers out undeclared classify [absent]. The claim was repeated by capabilities describe, by docs/generated/cli.md and by the site's registry dataset, and nothing noticed, because the only test of the CLI projection asked the registry whether it agreed with itself.

capability/closure.rs compares the two declarations:

  • a claim the command line does not answer is a failure — CLOSURE_CLI_ABSENT for a path clap does not have, CLOSURE_CLI_NOT_RUNNABLE for one that only groups other commands. The finding names the capability, the claim, the file that declares it, the file that declares the command line, and the command that shows it again.
  • a runnable command no capability claims is not a failure. serve and mcp start processes, generate writes files, and the worktree verbs call a service directly; none of them is a capability, and some never will be. The list is reported as the measure of how much of the command line is still hand-written rather than derived.

It is a pure function of the registry and the clap tree — no repository, no environment, no network — so it runs in a unit test, in tests/projections.rs, and as the projection line of capabilities validate, which CI runs through scripts/rust-check --integration.

majordomus capabilities projections answers the same matrix over the command line, over HTTP and over MCP, with --unmet for the failures alone.

Lifecycle and failure policy

discover the repository -> locate the share directory -> load kinds and schemas
  -> discover files through sources.yaml -> read and validate each into an object or a diagnostic
  -> build the registry (refuse on any invariant) -> construct the projection asked for -> serve or generate

The manifest, sources.yaml, the kinds files and the schemas are errors when they cannot be read: nothing can be discovered without them. Every other file that cannot become an object is excluded with a diagnostic naming its path and a stable code, the index reports degraded, and the projections still serve; --strict refuses a degraded index. A registry invariant violation is an error: no projection is served from a registry that does not build.

Extension

Adding an object of a known kind

Write the file where the repository's sources.yaml class for that kind looks, with the front matter or YAML its schema allows; track it (or run with --discovery filesystem); restart. It is a resource capability, an MCP resource, a member of objects.list, readable through objects.get over MCP and HTTP (one resolution of the URI, shared with the MCP resource read, which also answers majordomus://repository as repository.info's report), and listed by capabilities list. No Rust, no registration, no projection edited. apps/majordomus-cli/tests/external_extension.rs adds, removes and breaks objects between restarts and reads them back through every interface.

Adding a kind, with its schema, from the repository

.ai/repo/knowledge/kinds.yaml           schema: majordomus-kinds/v1 + one kinds: entry
.ai/repo/knowledge/schemas/note.schema.json   its JSON Schema
.ai/repo/knowledge/sources.yaml         a class mapping a pathspec to the kind

Then the objects. The same test proves it for a kind named note. A kind needing a new format or identity rule is a Rust change in index.rs; data describes objects, it does not define how a format is read.

Adding an executable capability

In the file of its module under apps/majordomus-cli/src/capability/builtin/:

  1. define the typed input and output (serde + schemars::JsonSchema; doc comments are the descriptions every client reads) and implement BenchmarkCases for the input (one or more representative inputs; a case may look at the index to name an object that exists),
  2. write one function fn(&Context, Input) -> Result<Output, CapabilityError>; the context carries the index, the registry, the peer board, the executor and, through an MCP session, the calling peer,
  3. add one capability! { id, title, description, input, output, stability, exposure, tags, handler } block to the module's capabilities: [...], with kind: CapabilityKind::Command after the id when it changes this process's memory, cache: CachePolicy::Process { .. } when a measurement says so, and .cancellable() after the block when the handler looks at ctx.progress.cancelled() and stops,
  4. run majordomus generate and majordomus capabilities validate (or just generate and just validate); commit the regenerated files under docs/generated/,
  5. add a behavioural test of the handler's semantics.

That is the whole workflow. MCP, HTTP, OpenAPI, Swagger UI, the capabilities commands, the benchmark targets on every exposed transport (with the cases from step 1), the cache behaviour, perf.counters, the reference, the benchmark matrix and the registry manifest follow from the block; the generic suites (tests/projections.rs, tests/bench.rs, tests/executor.rs, tests/properties.rs, tests/hot_path.rs) discover the capability through the registry and test it without an edit. A capability whose input type has no BenchmarkCases does not compile; one whose cases are empty for a repository fails coverage.

Adding a module

One Rust module under builtin/ with its module() built by module!, and one name added to compose_modules! in builtin/mod.rs. Its reference page, its rows in the matrix and its entry in the manifest are generated.

Generated projections and synchronization

majordomus generate [all|openapi|docs|benchmarks|registry|allow|providers|site|manifest] writes every artifact of the selected targets; majordomus generate --check derives them again, compares byte for byte, writes nothing, and exits 10 naming every stale file. CI runs the check. The site generator consumes registry.json, openapi.json and artifacts.json and nothing else of the crate; scripts/derive runs the two generators in dependency order and scripts/derive-check composes both checks (docs/GITHUB_PAGES_ARCHITECTURE.md). The committed files are caches: reviewable, never edited. What is generated is not written down here — docs/generated/artifacts.md is that list, and it is generated.

A derivation refuses an executable of another generation. Every artifact is a projection of the model compiled into the executable that writes it, so a binary built from another revision of the crate rewrites all of them from a model the tree does not have — silently, because nothing in the bytes says which executable produced them. A version string does not separate the two: on 2026-09-10 a binary calling itself the same version, passed in through MAJORDOMUS_BIN, reported eleven artifacts stale on a pristine origin/master and rewriting them removed 16,625 lines at exit 0. So generate asks, of a repository that declares this crate, three questions in turn and proceeds on the first one answered: the version must agree, because it is stamped into every provenance header; the generation — the digest generation::crate_generation takes over apps/majordomus-cli/{Cargo.toml,Cargo.lock,build.rs,src}, compiled in by build.rs — may equal the tree's, which means this executable was built from these sources; or the model it projects may equal the builtin half of the registry the tree committed at docs/generated/registry.json — the declarative kinds there are the repository's content, not the executable's model — which is what keeps a correctly-matching prebuilt MAJORDOMUS_BIN working without a rebuild. Otherwise it refuses with REFUSED (15) and writes nothing. Nothing is asked at all of a repository that does not declare this crate: a released binary generating a foreign repository is what a released binary is for. The verdict is deliberately not CONTRACT_UNMET (10): that is what a genuinely stale artifact reports, and its remedy — run the generator — is exactly what destroys the tree when the executable is the thing out of date. scripts/derive and scripts/derive-check propagate the two apart.

Every artifact is typed. It declares the document it projects, the encoding it is written in (json, yaml, markdown, text, matching its own suffix), the schema its content satisfies when the document has a contract, and the source it was derived from. The rule is project.generated-artifacts-are-typed@1; generate and generate --check verify the plan before a byte is written or compared and refuse a half-typed tree.

A structured document is written in every encoding it is committed in, from one value. generate::Document holds the value; the JSON and the YAML are two renderings of it, never two computations, and a document whose audience includes a reader also has a Markdown rendering of the same value. That is why the benchmark matrix in benchmarks.md cannot disagree with benchmarks.json.

Every artifact carries its provenance in the form its encoding allows: schema, generated and generator as members in JSON; x-majordomus-generated and x-majordomus-generator where the document's own specification fixes its member names (the OpenAPI document); a # comment banner in YAML and in line-oriented text; an HTML comment in Markdown. A provider bootstrap carries the majordomus update stamp of the policy it was rendered from instead. None carries a timestamp, an absolute path or a fingerprint that would move with a document edit.

A document that names a schema has a published contract. They live under share/schemas/generated/, one file per document, pinned to it by the const of its schema member — kind-schema discovery does not recurse, so nothing there is ever read as an object kind's schema. generate validates every document against its contract.

The whole set is indexed by docs/generated/artifacts.{json,yaml,md}, itself generated: every artifact with its encoding, contract, source, size and hash. Its own three encodings carry no hash — a document that hashed itself would have no fixed point — and --check compares them byte for byte instead. artifacts.list reads that manifest back and reconciles it with the working tree, which is what the MCP tool, the HTTP route, the Cockpit's Artifacts page and the site's /registry/artifacts/ all show.

When something fails

messagemeaningremedy
capability 'x' is defined twice: <a> and <b>two sources claim one idrename one, or delete the duplicate
MCP tool 'n' is claimed by 'a' and 'b', HTTP route GET /p is claimed by …, CLI path … is claimed by …two capabilities project to one namechange one exposure
invalid HTTP exposure: path '/x' is not under /api/v1/a route outside the versioned prefixmove it under the prefix
declares the execution policy …; its kind makes it …a descriptor carries a policy that is neither its kind's nor its kind's with cancellationlet the declaration classify itself, or add .cancellable()
… is planned and cannot be exposed as executable through MCP toola planned capability declares an executable exposuredrop the exposure until it is implemented
unknown_key … not in schema 'rule': ownera declarative file carries a key its schema does not allowremove the key, or extend the schema in the repository's schemas/ for a repository kind
schema_violation … class: "fatal" is not one of …a value fails a constraintfix the value
kind 'x' is declared by both share/kinds.yaml and .ai/repo/knowledge/kinds.yamla repository redefines a distributed kindrename the repository's kind
generated artifact(s) stale: docs/generated/openapi.json (differs)a committed projection no longer matches the registryrun majordomus generate and commit
this executable is not of this tree's generationthe executable was built from another revision of the crate and projects another modelrebuild it, or run scripts/derive with MAJORDOMUS_BIN unset — not majordomus generate, which is what rewrites the artifacts from the wrong model
capability 'x.y' (builtin …) is composed in module 'z' but its namespace is 'x'a capability! block sits in the wrong module's listmove it to the module its id names, or rename the id
module 'x' is composed twicetwo module! share an id, or a builtin module's id is a declarative kindrename one
invalid cache policy: a process cache with max_entries 0 keeps nothing, … a command changes state and is never cachedthe descriptor's cache policy contradicts itself or the kindfix the policy on the descriptor
FAIL benchmarks N of M requirement(s) missingan exposed executable's input type produced no case for this repositorymake BenchmarkCases return a case (or waive with a typed reason, which is reported)
bench --check: regression(s) founda metric grew over .ai/repo/benchmarks/rust/policy.yaml against this platform's baselinefind the cause with majordomus bench <id> and the phase timings, or record a new baseline deliberately
STALE <key> (in the baseline, not measured …)the baseline knows a target this run did not measure: renamed, removed, or filtered outbench baseline update after a rename or removal
no share directory holds kinds.yaml; tried …the distribution was not foundpass --share or set MAJORDOMUS_SHARE

Stability

status
the registry's invariants, both sources, deterministic buildbehaviourally verified (tests/registry.rs)
every declared projection present, no orphan, one id everywhere; a change to one descriptor reaches MCP, OpenAPI and the referencebehaviourally verified (tests/projections.rs)
HTTP over a real socket, OpenAPI, Swagger shell, typed errors, MCP and HTTP answering the same handler identically, a declarative object reaching HTTP and introspection untouchedbehaviourally verified (tests/http_serve.rs)
every route replayed over a socket with the capability's own benchmark cases, and the document showing those cases as examples, the modules as tags, the statuses of the kind as responses, the policies as extensions, one prose with MCP and the indexbehaviourally verified (tests/http_serve.rs, test/cases/92_openapi_reference.sh); timed by benches/routes.rs
the site's API reference rendered from the committed document, the raw document served at /openapi.json, externalDocs pointing at the pagechecked by scripts/generate-site-data --check and scripts/site-check (test/cases/92_openapi_reference.sh)
one shared server per repository: the lease, the bridge, /mcp sessions, the peers and their announcements, the fallback port, serve deferring, --standalone, the takeover after a kill, the re-attachment, the refusal when the taker cannot servebehaviourally verified (tests/mcp_shared.rs, tests/shared_units.rs, test/cases/90_mcp_shared_server.sh)
repository-defined kind and schema served without a code change; add, remove, breakbehaviourally verified (tests/external_extension.rs)
generate, byte-identical regeneration, --check on missing and tampered filesbehaviourally verified (tests/generate_check.rs)
modules compose capabilities, the root composes modules, the module invariantsbehaviourally verified (tests/registry.rs, test/cases/91_canonical_architecture.sh)
one executor; cache off, cold and warm agree; a hit runs no handler; errors and commands never cached; the bound; the fingerprintbehaviourally verified (tests/executor.rs, tests/properties.rs)
no request rebuilds canonical state after startup (counters over hundreds of real requests)behaviourally verified (tests/hot_path.rs, test/cases/91_canonical_architecture.sh)
every operation a benchmark target, generated denominator, exposure and policy propagation, real runners, baseline checkbehaviourally verified (tests/bench.rs, tests/bench_units.rs, test/cases/91_canonical_architecture.sh)
generated reference per module, benchmark matrix, registry manifest, deterministic and reconciledbehaviourally verified (tests/projections.rs)
the whole path through the shell tool's own initbehaviourally verified (test/cases/76_capabilities_projections.sh)
the id grammar, URIs, tool names, route paths, diagnostic codes, kinds.yaml, the schema filesimplemented; pre-1.0 compatibility surfaces, changes documented, never silent
Swagger UI assets offline or on the site, response examples, /openapi.yaml, path parameters, hot reload, mutation of the repository over any interface, a server-initiated stream on /mcpnot implemented; restart-based rediscovery is the contract, and a shared server keeps the index it built at start until its last client leaves

Claims this document defines

Each links to its own page with implementation, test and where it is used.