Skip to content

How Majordomus works

how the tool works as one repository model with many projections: authored, discovered, derived, observed and inferred state; the capability declaration and every surface derived from it, including where the command line and the Cockpit are still written by hand; the rule proof ladder from declared to evidenced; sessions, context, handover, planning, the peer board and the mesh; publication and versions; the surface, governance and mesh maps; how routes, navigation and order are kept true; the architecture debt, measured; and an answer key to the questions a technical reader should be able to answer

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

Majordomus is easy to misread as a command-line tool, an HTTP API, an MCP server, a dashboard and a folder of Markdown rules that happen to ship together. This document explains why that reading is wrong, and where it is still right. It follows a fact from the file where it is written, through the index and the typed registry that hold it, through the checks that decide whether it is true, to every interface that shows it — and it names each place where the repository does not yet live up to that shape.

Every statement here was checked against the code at the commit that last changed this file, and audited claim by claim by a second session. Where the code and a design record disagree, the code wins and the disagreement is listed under Architecture debt. Numbers are not written into this document: each one is stated as the command that measures it, because a count copied into prose is stale the day after it is written (project.no-counts-in-prose).

The thesis

Majordomus should discover and model a fact once, then derive every relevant interface from that canonical representation.

That sentence is not a code-reuse preference. It is the mechanism against drift. A repository worked on by several people and several AI workers at once accumulates descriptions of itself: a command list in the README, another in the help text, a third in an API schema, a fourth in a dashboard, a fifth in an agent's tool list. Each is correct on the day it is written, and each is maintained by whoever remembers it. More workers and more interfaces multiply the copies, and agentic development only makes the copies diverge faster.

Majordomus goes the other way: one model, many projections. A projection may render a fact differently, filter it, or join it with others. It may not hold a second opinion about it.

The architecture in one picture

flowchart TD
  d1_A["Authored canonical state: rules, ADRs, policy, claims, milestones, issues, capability declarations"]
  d1_G["Git and the filesystem"]
  d1_R["Runtime: server leases, MCP sessions, mesh datagrams"]
  d1_A --> d1_D["Discovery: kinds.yaml + sources.yaml + the capability composition"]
  d1_G --> d1_D
  d1_D --> d1_I["Object index: typed objects with canonical identity"]
  d1_D --> d1_CR["Capability registry: one descriptor per operation"]
  d1_I --> d1_CR
  d1_CR --> d1_X["One executor: Context::execute"]
  d1_R --> d1_O["Observed state: peers, standing, mesh nodes"]
  d1_X --> d1_V["Derived state: rule proof, claim evidence, plan status, waves, environment"]
  d1_O --> d1_V
  d1_V --> d1_P{"Projections"}
  d1_P --> d1_CLI["Command line: clap tree checked against the registry"]
  d1_P --> d1_MCP["MCP: tools and resources"]
  d1_P --> d1_HTTP["HTTP under /api/v1"]
  d1_HTTP --> d1_OA["OpenAPI 3.1"]
  d1_OA --> d1_SW["Swagger UI"]
  d1_P --> d1_CK["Cockpit: server-rendered pages"]
  d1_P --> d1_GEN["docs/generated and site data"]
  d1_GEN --> d1_SITE["majordomus.dev"]
  d1_GEN --> d1_CHK["generate --check, derive-check, projection tests"]

Read it from the top. Humans and workers author a small set of canonical files. Discovery finds them through declarations rather than hand-kept lists, the index gives each one a typed identity, and the capability registry holds every operation the executable can perform. Every surface asks the same executor. Derived state — whether a rule is proven, what an issue's status is, what a checkout's environment is — is computed from canonical and observed inputs; where a copy is kept (the committed projections, the environment cache), it is checked against or rebuilt from those inputs and never treated as a source. The generated files at the bottom are committed and checked, so a disagreement between the model and its projections fails a build instead of reaching a reader.

Two programs share one name

Before the model, one fact that shapes everything below. There are two executables, and both answer to majordomus:

ProgramLauncherWritten inOwns
the shell toolbin/majordomus with lib/*.shPOSIX shell and awkthe task lifecycle (start, check, finish), handover, context, session capture, doctor, doctrine dispatch, the plan engine
the Rust executablebin/majordomus-cliRust, apps/majordomus-cli/the capability registry, serve, mcp, HTTP, OpenAPI, Swagger UI, the Cockpit, generate, peers, mesh, environment, worktrees, rule proof, evidence, release

The shell tool refuses a command it does not know, and when docs/generated/cli.yaml — the generated description of the Rust command tree — lists it, names the launcher to run instead; it does not forward it. The two are converging on the Rust side (ADR 0047 and ADR 0052 describe the session cutover; both are still proposed), and some domains already have a Rust reader held to byte equality with the shell writer — the plan is the clearest case. The architecture described here is fully true of the Rust executable and partly true of the shell tool; the difference is recorded where it matters.

Six kinds of state

Each value Majordomus shows can be placed in one of these kinds. The kind decides who may write it and whether it can ever overwrite something else.

KindExamples in this repositoryWho creates itWho modifies itAuthorityMay overwrite canonical stateInvalidated by
Authored canonicalrules under .ai/repo/rules/, ADRs under .ai/repo/adrs/, .ai/repo/policy.yaml, docs/CLAIMS.yaml, milestones and issues under .ai/repo/project/, capability! declarations, share/design/tokens.yamla person or a worker, in a committhe same, in a reviewed commithighest: it is the sourcenot applicablea new commit
Discoveredthe object index, the module composition, the git worktree list, the tracked-file setthe executable, reading authoritative structuresnobody: it is re-readexactly that of its sourceneverthe source changing
Derivedrule proof state, claim evidence state, issue status and waves, the environment snapshot, the minimum release version, everything under docs/generated/a deterministic function of canonical and discovered inputsnobody by hand; the generator rewrites itnone of its own: it is a viewnever; generate --check refuses a hand editany input changing
Observedattached peers, server standing (ready, outdated, stale), mesh node presence, a service port answering, the dirty working treea probe at a momentthe next probetrue only at its timestampnevertime: a TTL, a disconnect, the next probe
Inferredthe intent relevance score in development context compilationa heuristicthe heuristicadvisory, ranked below every declared relationneverthe inputs changing
Local continuityhandovers, checkpoints, the active task, the open episode, the ledger under .ai/local/the shell tool on behalf of a workerappend-mostly, through commands onlyevidence of what a worker did, not of what is true nownever; a stale handover is shown as historyage and git distance

Two distinctions carry most of the weight.

Discovery is not inference. Discovery means the information already exists somewhere authoritative and Majordomus finds it: a rule file matched by a pathspec, a module named in the composition, a worktree listed by git. Inference means Majordomus proposes a likely interpretation. The only inference in the context compiler is keyword relevance, and it is capped below a direct declaration — a test, an_inference_is_never_as_confident_as_a_declaration, holds that — though not below every relation reached through weak or multi-hop edges (see Context for a session).

Observed is not canonical. A peer being online is not a declaration anybody made; it is the current reality, and it expires. Nothing in the peer board or the mesh registry is persisted as if it were a fact about the repository.

Discovery

Majordomus prefers finding repository facts in the structures that already own them over keeping parallel registries. The sources it actually reads:

SourceWhat is discoveredWhere
share/kinds.yamlthe document kinds the distribution defines: format, schema identity, identity fields, title fieldloaded by the index
a kinds.yaml beside sources.yamlkinds a repository adds, when it has any (it may not redefine distribution kinds)same
.ai/repo/knowledge/sources.yamlwhich git pathspecs hold objects of which kindsame
.ai/manifest.yamlwhich directory of .ai/ holds which section, and which are untrackedshell tool and executable
gittracked files, worktrees, branches, HEAD, the common git directoryapps/majordomus-cli/src/worktree/, lib/common.sh
the capability compositionevery executable operationapps/majordomus-cli/src/capability/builtin/mod.rs
the clap treethe command line as writtenapps/majordomus-cli/src/cli.rs
share/commands.yaml and the just recipe dumpthe shell tool's commands and the workflow recipesthe command graph
.ai/repo/ci/gates.yamlgates, path classes and what each gate runsscripts/ci-plan, rule proof
lease files under .ai/local/state/mcp/which checkout has a live server, and at which URLapps/majordomus-cli/src/lease.rs
UDP multicast, broadcast, rendezvous (off by default)other Majordomus nodesapps/majordomus-cli/src/mesh/
the docs/README.md indexwhich documents are published and in what orderscripts/generate-site-data

Discovery is explicit where it could have been magical. There is no link-time inventory and no build script scanning sources: ADR 0004 rejects both, so the composition of modules is an ordinary Rust expression a reader can open. That choice has a cost — adding a module means editing that composition — recorded under Architecture debt.

The index is built once per process from those declarations (apps/majordomus-cli/src/index.rs). To see what it holds in this checkout:

$ bin/majordomus-cli serve ensure
$ curl -s 'http://127.0.0.1:8741/api/v1/objects?kind=rule' | jq .
$ curl -s http://127.0.0.1:8741/api/v1/repository | jq .

The shared server of a checkout listens on the default port unless it is taken, and bin/majordomus-cli serve status prints the URL it actually holds.

Identity

A display name is for a reader. A canonical identity is for every relation. Majordomus never joins two things by their titles.

EntityCanonical identityExample
a capabilitya dotted id, namespaced by its modulerules.show
an indexed objectmajordomus://<kind>/<identity>majordomus://rule/majordomus.scope-integrity@1
an indexed object as a capability<kind>.<identity>rule.majordomus.scope-integrity@1
a ruleid plus version, never the file nameproject.derived-once at version 1
a claimits id in docs/CLAIMS.yamlscope-enforcement
a test in the evidence ledgerits runner and name, with the source path beside itsuite:04_start_check (source test/cases/04_start_check.sh)
a peer on a boarda position, p1, p2, reassigned on reconnect; the checkout it carries is the durable halfp7 in worktree prismatic-majordomus-wt/feature/x
a mesh nodea digest of its Ed25519 public keykept in ~/.local/state/majordomus/node.json
a worktreederived from the branch name: <repository>-wt/<branch>prismatic-majordomus-wt/docs/how-majordomus-is-derived

Identity is what makes the chain rule, validator, test, execution, evidence joinable by machine. A rule names its tests by path; the proof engine binds those paths to gates by what the gates run; the evidence ledger records executions by test identity and commit. No step matches text.

Schemas

There are two schema pipelines, one per kind of thing, and neither is written twice.

Executable operations. A capability's input and output are ordinary Rust types that derive schemars::JsonSchema. CanonicalSchema::of::<T> turns the type into one JSON Schema 2020-12 value (apps/majordomus-cli/src/capability/schema.rs), and that single value is rendered four ways: inline for MCP (for_mcp), as OpenAPI components (for_openapi), as the Cockpit runner form, and into docs/generated/registry.json. HTTP query parameters are coerced through the same schema. No capability schema is a hand-kept document — there is no TypeScript interface and no separate Swagger definition — though one Rust type, design::Ordered<T> (an ordered map), implements JsonSchema by hand rather than by derive.

flowchart LR
  d2_T["Rust type with derive JsonSchema"] --> d2_S["CanonicalSchema"]
  d2_S --> d2_M["MCP inputSchema and outputSchema"]
  d2_S --> d2_O["OpenAPI components"]
  d2_S --> d2_F["Cockpit runner form"]
  d2_S --> d2_J["docs/generated/registry.json"]
  d2_S --> d2_Q["HTTP query coercion"]

Repository documents. Each kind declared in share/kinds.yaml names a schema identity of the form <vendor>.<name>/v<n>. The schemas live under share/schemas/majordomus/<kind>/. Most Markdown kinds — rules, ADRs, skills, prompts, context documents and others (ls share/schemas/majordomus/*/*.proto) — are authored as protobuf messages and their JSON Schema is derived from them; the remaining kinds are authored directly as JSON Schema. The rule header, for example, is the closed message in share/schemas/majordomus/rule/rule.v1.proto, which is why an unknown front-matter key in a rule is refused rather than silently kept.

Generated artifacts have schemas too, under share/schemas/generated/. Before writing, generate checks every artifact for its provenance banner, and every structured document that declares a contract against its schema.

The capability: the one declaration

Every operation of the Rust executable is declared once, with the capability! macro (apps/majordomus-cli/src/capability/handler.rs). This is the real declaration of the operation that explains one rule's proof, from apps/majordomus-cli/src/capability/builtin/rules.rs (description shortened):

capability! {
    id: "rules.show",
    title: "What proves this rule",
    description: "One rule with its full proof: ...",
    input: RuleInput,
    output: RuleDetail,
    stability: Stability::BehaviorallyVerified,
    exposure: Exposure {
        mcp: mcp("majordomus_rule"),
        http: get("/api/v1/rules/rule"),
        cli: Some(CliExposure { path: vec!["rules".into(), "show".into()] }),
    },
    tags: ["rules", "governance", "verification", "evidence"],
    cache: CachePolicy::Disabled,
    handler: show,
},

What the declaration states, and what the macro fills in by itself:

Declared by the authorClassified or stamped, never declared
id, title, descriptionmodule, stamped by module! when the module is composed
kind (Query by default, or Command, Resource)provenance, from module_path!()
input and output typesavailability, visibility, execution, classified from kind and exposure
stabilitythe benchmark cases, taken from the input type's BenchmarkCases implementation
exposure for MCP, HTTP and the command linewhether it writes the repository: opt-in with .writes_repository(), pinned by the unit test the_capabilities_that_write_the_repository_are_these in capability/builtin/mod.rs to the set it lists
tags, cache, benchmark, handler

Two properties are enforced by the compiler rather than by review: an input type that does not implement BenchmarkCases does not compile — whether the operation is then timed is its declared BenchmarkPolicy (required, or waived with a typed reason), which the coverage report shows; and a handler whose signature does not match the declared types does not compile.

The registry

Modules compose capabilities with module!; compose_modules! composes modules; the application builds the registry with CapabilityRegistry::builder().with_modules(builtin::modules()).with_index(&index). The second call is where discovery meets the capability plane: every indexed object becomes a Resource capability (apps/majordomus-cli/src/capability/declarative.rs), readable over MCP as majordomus://<kind>/<identity>, so a new rule file is readable by an agent without a line of Rust.

build() validates the whole set and collects every error rather than stopping at the first (apps/majordomus-cli/src/capability/registry.rs): id grammar, namespace equal to module, duplicate MCP tool, duplicate HTTP route, duplicate command path, a malformed HTTP exposure, an invalid cache policy. A registry that would expose two operations at one route cannot be built.

There is one executor. Context::execute (apps/majordomus-cli/src/capability/executor.rs) is the only caller of the registry's dispatch. The command line, HTTP, MCP and the Cockpit all go through it, which is what makes "the same operation on every surface" a property of the code rather than a promise.

To inspect the registry of this checkout:

$ jq '.capabilities | length' docs/generated/registry.json
$ jq -r '.capabilities[] | [.id, .kind, .stability] | @tsv' docs/generated/registry.json
$ bin/majordomus-cli capabilities describe rules.show
$ bin/majordomus-cli capabilities validate

The projections

HTTP

There is no route table. The router asks the registry: Router::capability in apps/majordomus-cli/src/http/router.rs calls registry.by_http(method, path), answers 405 when another method matches and 404 when nothing does, binds a GET's query parameters through the input schema or takes a POST's JSON body, and calls Context::execute. Every HTTP exposure is forced under /api/v1/ by HttpExposure::PREFIX. Errors map to 400, 404, 422 and 500 in one place.

The non-capability mounts — the served home, /openapi.json, /swagger, /cockpit, /mcp, the events stream, /docs/, /rustdoc — are declared once in the web surface discovery (apps/majordomus-cli/src/web/; docs/generated/web.json lists them), which docs/WEB.md describes. A new operation needs no HTTP code at all: rules.show has no handler in the router, only the generic dispatch.

What is written by hand is the business logic behind the handler — apps/majordomus-cli/src/rules/ for the rule proof — never route metadata.

OpenAPI

apps/majordomus-cli/src/http/openapi.rs walks the registry and produces an OpenAPI 3.1 document. The operationId is the capability id, the schemas are the canonical schemas as components, the examples are the benchmark cases, and x-majordomus-* extensions carry the kind and identity. It is served live at /openapi.json from the same registry that serves the requests, and committed as docs/generated/openapi.json so a change to the contract is visible in review. One response is not yet derived faithfully: the document attaches the 422 refused response to commands only, while some queries can refuse too, and the router answers them with 422 all the same.

OpenAPI is derived from the same contracts that serve real API requests. It is not a second documentation to keep in step.

Swagger UI

Swagger UI at /swagger is a renderer of /openapi.json and nothing else (apps/majordomus-cli/src/http/swagger.rs). It is not a source of truth, holds no definitions, and cannot disagree with the API because it has nothing of its own to disagree with. Its browser assets are pinned to one version and loaded from a CDN — the one part of the local surface that does not work offline, and the module says so.

MCP

MCP is the agent-facing projection of the same registry (apps/majordomus-cli/src/mcp/surface.rs):

  • Tools are every capability whose exposure names a tool. inputSchema and outputSchema come from the canonical schema; the read-only hint comes from the kind. The destructive and idempotent hints do not yet come from the declared effect: they are written once in mcp/protocol.rs for every tool (see Architecture debt).
  • Resources are every capability whose exposure names a resource, plus every indexed object as majordomus://<kind>/<identity>, read through the same object resolution the HTTP objects endpoint uses.
  • A tool call resolves with registry.by_mcp_tool and runs Context::execute.
  • The instructions a client reads on initialize name the tools that write the repository, measured from each capability's declared effect, and OpenAPI publishes the same effect as x-majordomus-effect; every_surface_names_what_writes_the_repository_and_nothing_else in apps/majordomus-cli/tests/shared_units.rs holds both to the registry.
  • Prompts are not advertised and resource templates are empty — deliberately, not by omission.

No MCP tool is hand-written. majordomus_announce, which looks like a special coordination tool, is the ordinary capability peers.announce. The one hand-coded behaviour is in the MCP bridge (apps/majordomus-cli/src/mcp/bridge.rs), which recognises that tool so it can replay a client's announcements onto a new server after a reconnect.

The command line: a checked second declaration

This is where the model is honest about itself. The command line is not generated from the registry. The clap tree in apps/majordomus-cli/src/cli.rs is written by hand, dispatched by a hand-written match, and rendered by text functions in each command module. The source says so: apps/majordomus-cli/src/capability/closure.rs states that the command line "is declared a second time, in clap".

What keeps that second declaration from drifting is that it is checked in both directions:

  1. Declared to present. A capability whose CliExposure names a path that does not exist in the clap tree, or exists but cannot run, fails closure.rs and the projection test.
  2. Present to declared. Every clap command that does not render a capability must appear in cli::LOCAL (apps/majordomus-cli/src/cli/local.rs) with a typed reason — it renders a capability, writes the repository, is an alias, is session-local, or manages a process. quality::parity checks the list against the tree both ways, and scripts/ci/projection-check fails a command with no reason.
  3. Same executor. A command that renders a capability calls ctx.execute(id, input); its own code is presentation only (report_text, show_text in apps/majordomus-cli/src/commands/rules.rs), and --format json prints the capability's value unchanged.

In practice a command path is written three times: in the clap structs, in the capability's CliExposure, and as the literal words a command module hands to registry.by_cli to find its capability. The checks above keep the three in agreement; they do not make them one. Not every runnable command is a capability projection — many are LOCAL entries that render shared domain logic, write the repository, alias another command or manage a process. Measure the split rather than trusting a number here:

$ jq '[.. | objects | select(.executable? == true)] | length' docs/generated/cli.json
$ jq '[.capabilities[] | select(.exposure.cli)] | length' docs/generated/registry.json

Beside the hand-rendered commands there is a generic one, and it is derived. majordomus-cli run <capability-id> [--input JSON] (apps/majordomus-cli/src/commands/executions.rs) executes any registered capability through the same executor and prints its value — the command-line counterpart of the Cockpit runner. So every capability is reachable from the command line; a dedicated command with its own words and a human renderer exists only where a clap command was written for it. Indexed objects are read the same way, through entity kinds and entity show (capabilities entity.kinds and entity.show, also over HTTP and MCP), or through run objects.list.

$ bin/majordomus-cli run plan.status --format json
$ bin/majordomus-cli run peers.list --format json
$ bin/majordomus-cli entity show <uri>

Help text is clap's. Shell completion is not clap's: majordomus completion answers from the command graph, which composes the clap tree, share/commands.yaml for the shell tool and the just recipe dump (ADR 0027, docs/COMMANDS.md). The command reference on the website, /docs/cli/, is generated from docs/generated/cli.json, which is itself generated from the clap tree — so the reference cannot list a command the program does not have.

flowchart TD
  d3_CAP["capability! declaration with CliExposure"] --> d3_CL["closure check: path exists and runs"]
  d3_CLAP["clap tree in cli.rs"] --> d3_CL
  d3_CLAP --> d3_LOC["cli::LOCAL reasons for non-capability commands"]
  d3_LOC --> d3_PAR["quality::parity both directions"]
  d3_CLAP --> d3_CJ["docs/generated/cli.json"]
  d3_CJ --> d3_REF["/docs/cli/ reference pages"]
  d3_CLAP --> d3_CG["command graph"]
  d3_SC["share/commands.yaml"] --> d3_CG
  d3_JU["just recipes"] --> d3_CG
  d3_CG --> d3_COMP["completion"]

The Cockpit

The Cockpit is not another application, database or domain model. ADR 0012 calls it a projection of the registry for a person, and the code holds to that at the data level: the data every page shows comes through Context::execute (ask() in apps/majordomus-cli/src/cockpit/pages.rs), and it performs no discovery of its own. The mesh page renders mesh.status and mesh.nodes and holds no node list (project.mesh-is-observation-not-authority); the peers view renders peers.list.

It is server-rendered Rust with no template engine: a typed HTML tree (apps/majordomus-cli/src/cockpit/html.rs), components in view.rs, and browser libraries vendored as progressive enhancement. Its colours, type and status words are projections of share/design/tokens.yaml (ADR 0036), generated into the stylesheet the pages use.

What is derived and what is written:

PartHow it exists
the capability explorer and runner at /cockpit/capabilities/<id>generic: one page for every capability; a runner form, generated from the input schema with examples from the benchmark cases, for every capability exposed over HTTP whose stability allows execution; rules.show has no page code
the object browser at /cockpit/objects?kind=<kind> and /cockpit/object?uri=<uri>generic: every indexed kind, including rules, ADRs and claims
navigation cataloguesderived from the registry modules, the index kinds and the graph derivations, grouped by the product model's areas and ordered canonically
the top-level areas and their routeshand-written: a match in apps/majordomus-cli/src/cockpit/mod.rs and an area list in nav.rs
specialised pages (release, design, mesh, worktrees, continuity)hand-written views over typed capability answers

So there is no /cockpit/rules/<id> route. A rule is inspected at its object URI, and its proof by running rules.show in the runner. Every canonical entity is inspectable; not every kind has a bespoke page.

Documentation and the website

Three generated layers reach a reader, and each one is produced from a canonical source by a named generator and checked for drift:

OutputSourceGeneratorDrift check
docs/generated/ — registry, OpenAPI, command reference, capabilities reference, benchmarks, changelog, design, artifact manifest, graphthe registry, the index, the design tokens, the release records and git historymajordomus generatemajordomus generate --check, byte for byte
site/data/generated/ and site/content/ projections — docs pages, guarantees, doctrines, commands, registry pages, plan statusdocs/*.md, docs/CLAIMS.yaml, rules, share/commands.yaml, .ai/repo/project/, the generated registryscripts/generate-site-datascripts/generate-site-data --check
site/public/everything aboveZola via scripts/site-buildscripts/site-check, then publication verification

scripts/derive runs the whole graph in order (generate, site data, generate again because documents are indexed objects, then .gitattributes), and scripts/derive-check names every stale file. The artifact manifest docs/generated/artifacts.md lists every file majordomus generate writes, with its schema where it has one and its source, and is itself generated; the site generator's outputs are not in it.

This document is an example. It is authored once, here. scripts/generate-site-data discovers it through its row in docs/README.md — every top-level Markdown or YAML file in docs/ must have a row in one of the index's tables, or generation fails — writes site/content/docs/how-it-works.md with generated front matter (title from the first heading, description from the index row, order from the row's position), rewrites its relative links to site routes, and turns its mermaid blocks into rendered diagrams. The docs page is committed and held to the source by the same --check.

The surfaces compared

SurfaceConsumerPurposeDerived fromWritten by handFormat
HTTPsoftwareprogrammatic operationthe registry (routes, binding, dispatch)business logic onlyJSON under /api/v1
OpenAPIintegrators, generatorsthe contractthe registry and canonical schemasnothingOpenAPI 3.1 JSON
Swagger UIa person exploring the APIinteractive contract/openapi.jsonnothingHTML
MCPAI workerstools and resourcesthe registry and the object indexthe reconnect replay of announcementsJSON-RPC
Command linepeople and scriptsinteractive operationchecked against the registry; renders capability valuesthe clap tree, text renderers, cli::LOCALterminal text or --format json
Cockpitpeoplevisibility and running operationscapability answers; catalogues from registry and indexarea list, routes, specialised viewsHTML
Generated docs and sitereadersreference and explanationthe registry, the index, docs/explanatory proseMarkdown, JSON, HTML

They are interfaces over the same reality, not independent implementations of it. The differences are presentation, and the places where presentation is still hand-written are the places the checks watch.

Generator and renderer

A generator decides what exists: which entities, their relations, their order, their metadata. A renderer decides only how that looks. The rule proof shows the split:

flowchart TD
  d4_RF["rule files"] --> d4_IDX["object index"]
  d4_GY[".ai/repo/ci/gates.yaml"] --> d4_ENG["rules engine: apps/majordomus-cli/src/rules"]
  d4_IDX --> d4_ENG
  d4_LED[".ai/repo/evidence/ledger.json"] --> d4_ENG
  d4_ENG --> d4_RD["RuleDetail value"]
  d4_RD --> d4_T1["text renderer: rules show"]
  d4_RD --> d4_T2["JSON: rules show --format json and GET /api/v1/rules/rule"]
  d4_RD --> d4_T3["MCP tool majordomus_rule"]
  d4_RD --> d4_T4["Cockpit runner"]

No renderer computes whether a rule is proven. The frontend is not an authority over repository semantics: the Cockpit does not infer rule relationships, issue readiness or enforcement state, because the engine that owns each of them already answers.

Routes, navigation and order

Which routes and menus are derived, and which are declared on purpose:

SurfaceRoutesNavigationHow it is kept true
HTTPone per capability exposure, found by registry.by_http at request time; nothing registers a routeGET /api/v1/ lists every exposed capabilitythe registry refuses a duplicate route
OpenAPIone path per HTTP exposuretags from the capability namespacethe projection test compares every exposure with its operation
MCPone tool or resource per exposure, plus one resource per indexed objecttools/list, resources/listthe projection test; registry uniqueness
Cockpitgeneric pages for every capability and every object; the area routes are a hand-written list and matcharea list hand-written; the catalogues inside each area derived from the registry, the index and the product modeltests in cockpit/mod.rs hold the area list, the static routes and the dispatcher equal
websiteone page per derived object a projection publishes — features, claims (as guarantees), doctrines, rules, ADRs, commands, capabilities, use casessite/data/nav.toml, declared once on purpose: a navbar is editorial, a sitemap is notscripts/ci/nav-check and site-check: every href resolves, no href twice, every published section reachable
served homethe runtime's mounts, from the web topologyfrom the resolved topologyweb::discover and the reserved-path list

Order is a contract, not an accident. Generated lists sort by crate::order's key — group, then rank, then label, then canonical identity (ADR 0025) — so a projection lists the same entities in the same order on every run and every surface; the shell generators sort with LC_ALL=C. scripts/ci/order-check refuses a hand-rolled locale sort and ratchets the remaining ad-hoc sort sites, and the determinism tests generate twice and compare bytes.

How drift is prevented

Generation alone does not prevent drift; a generator nobody re-runs is a stale copy with a script beside it. What prevents it is that each projection is checked against its source.

InvariantMechanismWhere it runs
every declared projection exists, and no projection lacks a declarationevery_declared_projection_exists_and_no_projection_is_an_orphan in apps/majordomus-cli/tests/projections.rs: every MCP tool has the capability's id, schema and description; every HTTP exposure has an OpenAPI operation with the same id; every command path exists and runs; no orphan tool, resource or operation; every $ref resolvesthe crate's test suite in CI
a change to a canonical type reaches every projectiona_change_to_the_canonical_type_or_description_reaches_every_projection: swapping an input type changes MCP properties, OpenAPI parameters and the reference together; removing HTTP exposure removes the path and keeps the toolsame
generated output is deterministic and carries no machine pathgenerated_artifacts_are_byte_identical_twice_and_carry_no_absolute_path and the artifact tracing testsame
committed generated files equal a fresh generationmajordomus generate --check, scripts/generate-site-data --check, scripts/derive-checkscripts/rust-check and the site job in CI; scripts/pages current in the pre-commit hook
the capability surfaces agree end to end on a real binarytest/cases/76_capabilities_projections.sh: capabilities validate, one capability compared across its MCP metadata, OpenAPI operation and generated reference, counts compared both ways, generate --check failing after tampering, a new document kind added with data onlythe shell suite
every command has a reason to exist outside the registryscripts/ci/projection-check, quality::parityCI
the command graph is the one source of completion and referencesscripts/ci/command-graph, test/cases/101_command_graph.shCI
a generated file is not edited by handthe "GENERATED FILE — DO NOT EDIT DIRECTLY" header plus the byte comparison; project.derived-files-regeneratedpre-commit and CI

Determinism is a requirement, not a nicety: a projection that orders its entries differently on two runs makes every regeneration a diff and every drift check noise. Generated outputs sort by canonical identity within semantic groups (crate::order in the executable; C-locale sorts in the shell generators), and the determinism tests generate twice and compare bytes.

One capability through every surface

rules.show, from declaration to evidence. The last column is the point: almost nothing after the declaration is written per capability.

StepFile or URLAuthored or derived
declarationapps/majordomus-cli/src/capability/builtin/rules.rsauthored
input type and its benchmark casesRuleInput in the same fileauthored
domain logicapps/majordomus-cli/src/rules/authored
compositionthe rules module in builtin::modules()authored, two existing lines: pub mod and the compose_modules! entry
registry validationCapabilityRegistry::buildautomatic
HTTP routeGET /api/v1/rules/rule?rule=<id> via the generic routerautomatic
OpenAPI operation rules.show/openapi.json, docs/generated/openapi.jsongenerated
Swagger UI entry/swaggerrendered
MCP tool majordomus_rulemcp/surface.rs, genericautomatic
Cockpit runner/cockpit/capabilities/rules.show, form from the schemaautomatic
command majordomus-cli rules show <id>clap variant in cli.rs, dispatch and text renderer in commands/rules.rs, a runnable example in cli.rsauthored, then checked by closure and parity
reference docsdocs/generated/capabilities.md, docs/generated/modules/rules.md, docs/generated/cli.mdgenerated
website/registry/capabilities/rules-show/, /docs/cli/rules/show/generated
benchmarksdirect, MCP and HTTP transports from the benchmark casesgenerated
teststhe per-module projection test, tests/projections.rs, the CLI example test, apps/majordomus-cli/tests/rules.rsauthored once, generic where possible

What happens when a capability is added

The honest developer workflow on the default branch today:

  1. Write the domain behaviour in its own module under apps/majordomus-cli/src/.
  2. Declare the capability with capability! in a module under apps/majordomus-cli/src/capability/builtin/, with input and output types deriving JsonSchema and an input implementing BenchmarkCases. If the module is new, add it to the pub mod list and to builtin::modules() — the one manual composition step.
  3. HTTP, OpenAPI, Swagger UI, MCP and the Cockpit runner appear with no further code, for each exposure the declaration names; the runner needs an HTTP exposure, and run <id> reaches it from the command line either way.
  4. If the capability is exposed on the command line, add the clap variant, dispatch it with ctx.execute, write the text renderer, and add a runnable example. Closure and parity fail capabilities validate, the projection test and projection-check until the declaration and the tree agree; the build itself succeeds.
  5. Run just derive (scripts/derive: generate, site data, generate again, then .gitattributes), which rewrites the registry, OpenAPI, the references, the benchmark inventory and the site data; majordomus generate alone does not rewrite the site data. Commit them with the change. The pre-commit hook refuses a commit whose site data is stale and, when the executable is built, one whose registry projections are stale.
  6. Tests. The generic projection tests cover the new capability automatically. A module's own pin test lists its capabilities and their exposures, so adding one to an existing module means extending that table; then add a behavioural case for what it does.
  7. Claim and evidence. If the capability is a public guarantee, add a claim to docs/CLAIMS.yaml with its test, and a detail page under docs/claims/; record the test's run with majordomus-cli evidence record.
  8. Version. The public surface changed, so majordomus-cli release analyze now owes a bump; release bump writes it (see Publication and versions).

Nothing is registered separately in HTTP, OpenAPI, Swagger, MCP, the Cockpit or the reference. The command line is the remaining second declaration, and it is a checked one. The zero-registration goal — a new canonical entity reaching every relevant surface with no per-surface step — is met for every surface except the command line and module composition.

Governance: what each document kind is

The taxonomy as the schemas and loaders implement it:

KindWhat it isAuthored atSchema
rulea versioned invariant with a statement, a class (blocking or advisory), dependencies, and optionally a declaration of how it is enforced.ai/repo/rules/project/ and the vendored standard under .ai/repo/rules/vendor/majordomus/rules/majordomus.rule/v1
doctrinenot a separate kind: a rule whose enforcement block names a validator that a command runsderived from rules by lib/doctrine.shthe rule schema
policyconfigurable repository behaviour: profiles in force, required handover sections, freshness thresholds, enforcement wiring.ai/repo/policy.yamlmajordomus.policy/v1
ADRan architectural decision with its context, alternatives and consequences; proposed until a person accepts it.ai/repo/adrs/majordomus.adr/v1
claima public statement of what the tool guarantees, with its source, implementation, test and statusdocs/CLAIMS.yaml, detail in docs/claims/majordomus.claim/v1
profilea named set of policy toggles for a kind of work.ai/repo/profiles/majordomus.profile/v1
skilla packaged procedure for a worker.ai/repo/skills/majordomus.skill/v1

The doctrine is the part most often misunderstood. A doctrine is not a principle document that rules point at; it is a rule the tool itself enforces through a named validator. The registry of doctrines is derived, never written: it is every rule of the effective set whose enforcement block names a validator (docs/DOCTRINE.md). The principles a doctrine serves are themselves rules — the vendored rules tagged principle (files principle-NN-*.v1.md, for example majordomus.one-worker-one-scope) — connected by ordinary depends_on. There is no typed doctrine-to-rule field; see Architecture debt.

Doctrines come from a package. The standard rules live in share/standard/majordomus/, with a manifest carrying each file's sha256, and are vendored byte for byte into the repository. majordomus doctor compares the vendored files with the manifest.

A rule's shape

An excerpt of the real header of .ai/repo/rules/project/rule-is-a-doctrine.v1.md (its description omitted, its statement shortened):

id: project.rule-is-a-doctrine
version: 1
kind: rule
title: A new enforced rule is a doctrine, not an inline check
statement: A rule the tool enforces declares an x-majordomus block ...
status: active
class: blocking
depends_on: []
tags: [doctrine]
x-majordomus:
  tests: [test/cases/18_doctrine_wiring.sh, test/cases/125_rule_proof.sh]

The body must carry the headings rule.v1.proto requires: Rationale, Required behaviour, Failure behaviour, Verification. The x-majordomus block has no mode field. The enforcement mode is derived from what the block names (ADR 0048):

The block namesModeMeaning
a validatordispatcheda command runs mj_validate_<name> itself: check, finish, doctor, watch — whichever its enforced_by lists
tests and no validatorgateda CI gate runs the tests that prove it
only reviewed_becauserevieweda person enforces it, and the rule says why no machine can
nothingdeclarativea stated principle; allowed only for the vendored rules tagged principle — any other rule naming nothing fails rule-proof-check

The shell rule loader (lib/rules.sh) refuses a half-declared block; the Rust proof engine reads one as declarative and reports it.

The lifecycle of a rule

flowchart TD
  d5_AU["authored: a .vN.md file"] --> d5_PA["parsed against rule.v1"]
  d5_PA --> d5_IX["indexed: majordomus://rule/id@version"]
  d5_IX --> d5_MO["mode derived from x-majordomus"]
  d5_MO --> d5_GB["gates bound: tests matched to what gates.yaml runs"]
  d5_GB --> d5_EX["executed: a gate or a command runs"]
  d5_EX --> d5_RC["recorded: evidence ledger row with commit and digest"]
  d5_RC --> d5_ST["state derived against the current tree"]
  d5_ST --> d5_SF["surfaces: rules report, rules show, MCP, Cockpit, site"]

A rule existing is not a rule being enforced

This is the central governance idea, and the implementation types it. A rule can stand at any of these rungs, and each rung is a separate fact:

RungWhat establishes it
declaredthe file exists
parsed and validthe loader and the rule schema accept it
registeredthe index holds it at its URI
proof namedits x-majordomus block names a validator or tests
proof resolvesevery named path exists in the tree — otherwise dangling, which always fails
wiredthe validator is dispatched by a command (checked by the doctrine-wiring doctor check), or a gate in .ai/repo/ci/gates.yaml runs the named test
executeda run happened
recordedthe evidence ledger holds that run, with its commit, working-tree state and digest
currentthe recorded run's inputs are unchanged in the tree being asked about

The Rust rules engine (apps/majordomus-cli/src/rules/mod.rs) reduces this to one state of RuleState, strongest first: proven, inputs_unchanged, stale, gated, failing, not_run, reviewed, unrunnable, dangling, unproven. A dangling path makes the rule dangling — it dominates everything; otherwise a failing test makes it failing; otherwise its state is the weakest among the tests that can carry proof. Only the first three count as passing. A finding is raised for a dangling proof, or for a blocking rule that is unproven, failing or unrunnable; not_run, gated and reviewed are deliberately not findings, because they describe the absence of a record rather than a defect.

scripts/ci/rule-proof-check is the gate: a dangling proof fails, and a rule of any class that names no validator, no test and no reviewed_because fails — the class is not an exemption. The one exemption is a vendored rule tagged principle. It is held by a ratchet whose baseline, .ai/repo/rule-proof-baseline.txt, is empty.

To see where the repository stands — not to take this document's word for it:

$ bin/majordomus-cli rules report
$ bin/majordomus-cli rules show majordomus.scope-integrity
$ curl -s http://127.0.0.1:8741/api/v1/rules | jq .summary

At the time of writing that report says every blocking rule either names a proof that resolves and is bound to a gate, or declares reviewed_because and is enforced by a person (rules report --class blocking lists which), and that few runs are recorded in the ledger, so most rules stand at not_run rather than proven. That is the correct reading of the evidence: gates run in CI on every change, and CI records its runs into a per-run artifact (scripts/ci/evidence-collect), but nothing yet carries them into the tracked ledger (ADR 0080, proposed). "Satisfied" in the report means no finding — nothing dangles, and no blocking rule is unproven, failing or unrunnable; it includes reviewed rules and advisory rules that name no proof. It does not mean a recorded run passed. A green badge anywhere in Majordomus has to be a projection of a recorded execution; where none is recorded the surfaces say not run, gated or reviewed — never proven.

The governance map

flowchart LR
  g_P["principle: a vendored advisory rule tagged principle"] -->|depends_on| g_R["rule: id@version, class, statement"]
  g_R -->|x-majordomus names a validator| g_V["validator: mj_validate_* in lib/"]
  g_R -->|x-majordomus names tests| g_T["tests: test/cases, crate tests, gate scripts"]
  g_R -->|names only reviewed_because| g_H["a person's review"]
  g_V --> g_D["dispatched by check, finish, doctor, watch"]
  g_T --> g_G["bound to gates in .ai/repo/ci/gates.yaml"]
  g_D --> g_X["execution"]
  g_G --> g_X
  g_X -->|evidence record| g_L["ledger: outcome, commit, tree, digest"]
  g_L --> g_S["proof state, judged against the presented tree"]
  g_S --> g_U["rules report, rules show, MCP, Cockpit runner, site"]

A doctrine is the upper path — a rule whose block names a validator that a command dispatches. A gated rule is the middle path. A reviewed rule is the lower one, and no surface calls it enforced by a machine.

Evidence

Evidence is a recorded execution, not a test path that happens to exist (docs/EVIDENCE.md).

flowchart LR
  d6_TR["test run"] --> d6_REC["majordomus-cli evidence record"]
  d6_REC --> d6_LED[".ai/repo/evidence/ledger.json: latest run per test"]
  d6_LED --> d6_JOIN["join: claim or rule to its tests"]
  d6_TREE["git: diff from the recorded commit"] --> d6_JOIN
  d6_JOIN --> d6_STATE["proof state"]
  d6_STATE --> d6_OUT["evidence report, rules show, Cockpit, site guarantees"]

The ledger is committed and holds the latest execution per test: runner, outcome, seconds, commit, working-tree state, a digest, the origin (local, CI or release) and the command. A claim's proof state is derived on request:

StateMeaning
proventhe recorded run passed, nothing but the ledger changed between its commit and the tree being asked about, and both trees were clean
inputs_unchangedthe claim's own source, implementation and test are unchanged, but something else changed or one of the two trees was dirty
staleone of those inputs changed, the presented revision does not contain the run's commit, the claim declares no inputs, or git could not answer
failingthe recorded run failed
not_runno run is recorded
unrunnablethe named test is of a kind no runner can execute
no_testthe claim names no test

The decision lives in one function, freshness() in apps/majordomus-cli/src/evidence/freshness.rs, so no surface can judge a run differently. Freshness is therefore not a timestamp but a relation to the tree: a test that passed at one commit is proven at that commit, inputs_unchanged at a later commit that did not touch its inputs, and stale at one that did. proven and inputs_unchanged are never collapsed into one state, because the second is an inference about relevance and the first is not.

The evidence capabilities are declared once in apps/majordomus-cli/src/capability/builtin/evidence.rs and listed by jq -r '.capabilities[] | select(.id | startswith("evidence.")) | .id' docs/generated/registry.json: the report, one claim, one test, the recording and the stamp. evidence.record is the only writer and is command-line only, and so is evidence.stamp, which reads a path its caller names.

One doctrine traced: scope integrity

majordomus.scope-integrity is a doctrine: a vendored rule whose block names a validator.

LinkWhere
authoredshare/standard/majordomus/rules/scope-integrity.v1.md, vendored to .ai/repo/rules/vendor/majordomus/rules/scope-integrity.v1.md, sha256 in both manifests
principledepends_on: majordomus.one-worker-one-scope@1
enforcement blockvalidator: scope, enforced_by: [check, finish, watch], exit_code: 10, claims: [scope-enforcement, scoped-task], tests: [test/cases/04_start_check.sh]
validatormj_validate_scope in lib/check.sh: fails files changed since the task's start that fall outside the task's scope
dispatchmj_doctrine_dispatch in check, finish and watch; finish --check runs from the pre-push hook
wiring verifiedmj_validate_doctrine_wiring in lib/doctor.sh, mutation-tested by test/cases/18_doctrine_wiring.sh
testtest/cases/04_start_check.sh, run by the shell-suite and macos gates
claimscope-enforcement in docs/CLAIMS.yaml, detail at docs/claims/scope-enforcement.md
index and MCPmajordomus://rule/majordomus.scope-integrity@1; tool majordomus_rule
HTTPGET /api/v1/rules/rule?rule=majordomus.scope-integrity
Cockpit/cockpit/object?uri=majordomus%3A%2F%2Frule%2Fmajordomus.scope-integrity%401
website/doctrines/scope-integrity/, from site/data/generated/doctrines.json; /guarantees/scope-enforcement/
evidencewhatever the ledger holds for test/cases/04_start_check.sh, read with rules show

One rule traced: derived files are regenerated

project.derived-files-regenerated is a gated project rule: no validator, tests named.

LinkWhere
authored.ai/repo/rules/project/derived-files-regenerated.v1.md
prooftests: naming test/cases/56_derived_current_gate.sh, test/cases/51_derived_artifacts_committed.sh, test/cases/95_executable_reference.sh and scripts/derive-check
enforcement pointthe pre-commit hook runs scripts/pages current; .ai/repo/policy.yaml declares that wiring as derived-current, and doctor fails if the hook stops running it
gatesshell-suite runs the named cases, and macos on demand; scripts/derive-check is classed as unrunnable by the proof engine and ignored because the others resolve
surfacesrules show project.derived-files-regenerated, the object page in the Cockpit, its own page /rules/project-derived-files-regenerated-1/, and the rules section of /features/declare-once/
evidenceread with rules show; the proof is named and gated

Walkthrough: adding a new rule

Suppose a maintainer adds: every public capability has at least one executable verification. What happens at each step on the default branch today:

  1. Authored. A file named every-capability-is-verified.v1.md under .ai/repo/rules/project/, with the rule header and the required body headings. Manual: the file, its relations and its enforcement block are the authored parts.
  2. Schema. The rule loader validates the header against rule.v1; an unknown key or a half-declared enforcement block is refused. Automatic.
  3. Discovery. The rule source class in .ai/repo/knowledge/sources.yaml already matches the directory, so the index holds majordomus://rule/project.every-capability-is-verified@1. Automatic.
  4. Relations. depends_on names the principle it serves, by id@version; the DAG is validated by doctor. Authored, then checked.
  5. Enforcement. The author chooses the mode by what the block names. Much of this rule is already true by construction — an input without benchmark cases does not compile, and the projection tests run every exposure — so a tests: list naming apps/majordomus-cli/tests/projections.rs and a new case makes it gated. Authored.
  6. Gate binding. The proof engine binds those tests to the gates in .ai/repo/ci/gates.yaml that run them. Automatic; a path that does not exist is dangling and fails rule-proof-check.
  7. Command line. majordomus-cli rules report and rules show include it. Automatic.
  8. HTTP. GET /api/v1/rules/rule?rule=project.every-capability-is-verified. Automatic.
  9. MCP. Resource at its URI and the majordomus_rule tool. Automatic.
  10. Cockpit. Its object page and the rules report in the runner. Automatic.
  11. Website. The rule gets its generated page at /rules/project-every-capability-is-verified-1/; a doctrine also gets a /doctrines/ page, and a feature that names it lists it. Automatic.
  12. Execution. CI runs the gates on the next change that touches their path class. Automatic.
  13. Evidence. A run reaches the ledger only through evidence record. Manual today.
  14. Current state on every surface. Derived on request from the ledger and the tree. Automatic.
  15. Regression. The gate fails a future change that breaks the invariant; rule-proof-check fails a future edit that removes the named test. Automatic.

Sessions, tasks and continuity

Work survives the session doing it through durable records, not transcripts (docs/CONTINUITY.md). The records are written by the shell tool:

RecordWherePersistentAuthored or derived
active task.ai/local/state/current.yamllocaltask, scope, profile authored; a computed from git block derived
open episode.ai/local/state/sessions-open/local, until closederived from provider hook events
closed session.ai/repo/sessions/ when the manifest names that sectioncommittedderived from the ledger at close
checkpoint.ai/local/state/checkpoints/localderived with checkpoint --derive, or authored
handover.ai/local/state/handovers/localbody authored; front matter derived
ledger.ai/local/state/ledger.jsonllocal, appended to and rotated under the policy's ledger.retention_max_linesderived: one receipt per event

Provider hooks (for Claude Code, under .claude/hooks/) call majordomus capture session on start, compact and end. Start opens or keeps the episode, makes sure the checkout's server is running and prints a briefing; end derives a checkpoint and a handover and closes the provider's own session. .ai/local/ is git-ignored: continuity is local to a machine, and only closed session records are committed. On a repository with mesh cooperation enabled, handovers also replicate to linked runtimes through the journal (apps/majordomus-cli/src/mesh/handover.rs).

The task lifecycle is start with a required scope, check which dispatches the doctrines bound to it, and finish with an outcome and, when the profile requires, a verify command whose exit code is recorded. A finish that fails a doctrine is refused with exit 10 and the list of what is unmet.

Context for a session

Context is compiled in several places, and each is deterministic. Besides the three below, lib/session_context.sh composes the open episode's working context on every read.

majordomus context (lib/context.sh) assembles ordered sections — git, peers, task, profile, context documents, open questions, knowledge candidates, decisions, reasoning, checkpoint, handover, integration, relevant files, history, prompt — gated by profile toggles and fitted to the policy's line budget. Sections are dropped least reliable first — history, integration, files, documents, knowledge, reasoning, decisions, then the checkpoint and handover bodies, which shrink to a pointer line before they go, then peers — and git, task, profile and open questions are never dropped. Every drop is listed under EXCLUDED; if the output is still over budget the command exits 10 instead of truncating silently.

majordomus context resolve <path> (lib/context_docs.sh) composes the context documents that apply to a path: every context/v1 Markdown file under .ai/ whose scope covers it — exact directory, containing subtree, or an explicit path list — ordered by depth, then declared order, then path, with superseded documents removed and a fingerprint over the chain. Each document adds to its ancestors; the nearest does not replace them.

devcontext.compile is the Rust compiler of a ranked working set for a task (apps/majordomus-cli/src/devcontext/). It walks the composed object graph from the task along typed edges with fixed forward and reverse weights, sorts by authority tier — task, governance, source, decision, knowledge, history — then relevance, then URI, and spends a token budget first fit, naming every item it skipped and its cost. Its one inference is intent: words of the task matched against an object's title, description, identity and tags, capped below a direct seed and below most single declared edges (devcontext/select.rs); it can still outrank a declared relation reached through a weak or multi-hop edge, and the authority tier is sorted before relevance. The shell context command does not use it yet.

Context is generated because it must be rebuildable. A context that exists only inside an earlier prompt is lost with that prompt; a context compiled from repository state can be compiled again by the next worker and explained item by item.

Handover

majordomus handover takes a body on stdin with the sections the policy requires — Objective, Current State, Next Action — or derives one with --derive. The body is authored; the front matter is derived: repository, worktree, branch, head, working-tree state, changed files, timestamps. A body that carries identity fields is refused, so a worker cannot claim a head it is not at.

handover --resolve finds the most relevant record: same worktree and branch first, then same branch, never repository-wide. It labels the relation to the current HEAD as exact, advanced, diverged or a different context, and prints the freshness verdict from the policy's thresholds, marking a stale record as history. The session briefing applies the same verdict and does not quote a stale handover's next action, because an instruction that was true days ago is context, not an order.

Planning

Milestones are outcome specifications; issues are execution contracts (docs/PLANNING.md). Both are authored YAML under .ai/repo/project/. An issue declares dependencies, scope, acceptance criteria, validation, the evidence it requires and the timestamps of its transitions. It has no status field. Status is derived:

flowchart TD
  d7_I["issues: depends_on, timestamps, evidence_required, evidence covers"] --> d7_S["status per issue"]
  d7_S --> d7_W["waves: Kahn layering of the dependency DAG"]
  d7_W --> d7_C["scope_conflict warnings: same wave, shared paths"]
  d7_M["milestones: required issues, milestone dependencies"] --> d7_MS["milestone status"]
  d7_S --> d7_MS
  d7_MS --> d7_N["active milestone and next issue"]
  d7_N --> d7_P["docs/PLAN_STATUS.md, plan capabilities, Cockpit, site"]

An issue is done when it is not cancelled, has a completion time, and every evidence token it requires is covered; otherwise it is the first of verify, active, blocked or ready that its timestamps and dependencies allow. A status ahead of its blockers is reported as premature execution. Issues on or downstream of a cycle get no wave and are reported. A milestone blocked by its dependency blocks its issues.

The engine is lib/project.awk; apps/majordomus-cli/src/plan.rs is a second reader held to byte equality by test/cases/99_plan_capabilities.sh. The one writing capability, plan.transition, moves an issue only when the derived plan allows it, and refuses to mark it done when the simulated plan after the move would not derive done. There is no stale second status to maintain: the status is a function of the records.

The peer board, overlap and the mesh

Three things are easy to conflate: who else is working in this repository right now, whether their work collides with mine, and which Majordomus instances exist on the network.

The board: what peers are doing

Each checkout's shared server keeps a peer board in memory (apps/majordomus-cli/src/peers.rs). An MCP client that attaches becomes a peer with a position id, its client name, transport, connection time and last activity. With majordomus_announce it states an intent and a scope, optionally under a claim name, and announcing again under the same name replaces that claim. The board is not persisted: a departed peer that had announced is retained as not attached, up to RETAINED departed slots (apps/majordomus-cli/src/peers.rs), and one that never announced disappears.

majordomus_peers gathers the board repository-wide (ADR 0044): it reads this checkout's board from memory and asks every sibling worktree's server over HTTP, one hop, stamping each peer with the checkout it came from. complete: false means a board could not be read, and boards says which and why — a short board is not proof that nobody is there.

Overlap: derived, advisory

Overlap is derived from claims, never declared. Two scopes meet when they are equal or one contains the other on a path boundary. It is computed in three places:

WhereComparesResult
peers.announce and peers.listannounced scopes on the boardan overlap list; informational
majordomus start and check --overlap (lib/start.sh)the active task scopes of other worktreesINFO findings; always exit 0
majordomus contextboard claims against the active task's scopean OVERLAP line: "a claim is not a lock"

None of them refuses anything. Announce-time overlap compares only the announcing checkout's own board; the repository-wide comparison is made when the board is gathered, by peers.list and the context briefing. What refuses on one machine is the scope doctrine: files a task changed outside its own scope fail check and a completed finish, and finish --check runs from the pre-push hook. scripts/collision-check compares unmerged local and remote branches for paths both add with different content, and the collision-check gate refuses such a collision on a pull request (exit 11) unless it is labelled accepted. Across machines, a claim taken with mesh.claim is exclusive by default and is refused with claim_conflict when it meets a live exclusive claim of another session — among claims, the one that is more than awareness (see The mesh: who exists).

flowchart LR
  d8_A["worker A: task scope, announced claim"] --> d8_B["board in A's server"]
  d8_C["worker B in another worktree"] --> d8_D["board in B's server"]
  d8_B --> d8_G["peers.list gathers every checkout's board"]
  d8_D --> d8_G
  d8_G --> d8_O["overlap: scopes equal or nested"]
  d8_O --> d8_V["CLI context, MCP peers, Cockpit"]
  d8_T["task scope in current.yaml"] --> d8_SC["scope doctrine at check, finish, pre-push"]

The mesh: who exists

The mesh answers a different question: which Majordomus nodes exist and can be reached (docs/MESH.md, apps/majordomus-cli/src/mesh/). A node has an Ed25519 identity kept per user; an instance is one run of a process. Discovery providers send one signed, bounded envelope over UDP multicast, UDP broadcast or an HTTP rendezvous endpoint, and a declaration may name static cooperation seeds. The registry holds nodes by identity with a presence TTL and a retention window; trust is deny-unknown; it labels nodes and decides which may link and replicate the journal, and it grants no execution rights.

FieldKind of state
node identity (public key digest)persistent, per user
instance, address, last seenobserved; expires
trust labelauthored policy applied to an observation

A repository with no mesh declaration, or one declared enabled: false, opens no discovery socket, and the skeleton a new repository starts from ships none. This repository's own declaration, .ai/repo/mesh/majordomus.yaml, is enabled: multicast on the local segment, rendezvous hubs on the owner's private network and tailnet, and the trusted keys it lists (docs/MESH.md, "This repository's mesh"). There is no Tailscale or mDNS provider; ADR 0050 lists them as future providers, and a rendezvous endpoint reachable over a tailnet is the supported way to span machines. Data flow is the same as every other surface: mesh.status and mesh.nodes capabilities, served over HTTP and MCP and rendered at /cockpit/mesh.

The board is projected into the mesh. The board still gathers the checkouts of one machine through lease files, and that remains the machine-local view. Above discovery, cooperation links the runtimes of one repository — a server per checkout, on this machine or another — over authenticated links (ADR 0067): every heartbeat a runtime's board sessions become mesh sessions and their announcements advisory claims, in one signed journal that every linked runtime replicates and folds the same way. So the peer board and the claims replicate across runtimes; a link needs an enabled mesh and a trusted key. The journal is a signed, per-stream sequence with a Lamport clock (apps/majordomus-cli/src/mesh/journal.rs) carrying session, claim, handover and review events, and its state is a pure fold (mesh::state::fold): a board announcement becomes an advisory mesh claim, while a claim taken with mesh.claim is exclusive and refused on conflict. Under a partition both sides may claim; the fold picks the lowest Lamport clock, then stream, then sequence, and marks the loser conflicted — the claims are not linearisable, and docs/MESH.md says so. The node registry and the link table live in memory and die with the process; the identity file and the journal persist, and a reloaded journal restores events but no freshness. The module's stability is experimental. The mesh does not orchestrate agents.

The mesh map

flowchart TD
  m_M["machine"] --> m_N["node: Ed25519 identity per user and machine"]
  m_N --> m_R["runtime: one per checkout's shared server"]
  m_R --> m_B["board: attached MCP peers and their announcements, in memory"]
  m_R --> m_S["mesh sessions in the signed journal"]
  m_B -->|every heartbeat| m_AC["advisory mesh claims"]
  m_S --> m_EC["exclusive claims via mesh.claim"]
  m_AC --> m_SC["scopes"]
  m_EC --> m_SC
  m_SC --> m_O["overlap: equal or nested paths, reported"]
  m_EC -->|meets a live exclusive claim| m_CC["claim_conflict: refused"]

Presence, links and the board are observed and expire; the identity and the journal persist; an overlap is derived from claims and reported; only an exclusive mesh claim is ever refused.

The repository environment

RepositoryEnvironment (apps/majordomus-cli/src/environment/mod.rs) is one typed snapshot of what a checkout is: project, repository, version control, toolchains, layer, workflows, provider projections, local services and diagnostics, with a provenance entry for every value (docs/ENVIRONMENT.md). It resolves fast from a cache under .ai/local/ or fully from the index.

The model is the architecture; the banner is one renderer of it. The same value is rendered as the shell banner through direnv (environment/render.rs), as majordomus-cli env text and JSON, as GET /api/v1/environment, as the MCP resource majordomus://environment, and explained by environment.explain. A local service is observed with a single bounded TCP connect, which proves a port answers and nothing more.

Publication and versions

From a merge to a public page

flowchart TD
  d9_M["merge to master"] --> d9_B["scripts/pages build: refuse stale source hash, render without regenerating"]
  d9_B --> d9_K["scripts/pages check: site-check and the never-published check"]
  d9_K --> d9_P["scripts/site-deploy pushes site/public to gh-pages"]
  d9_P --> d9_V["scripts/pages verify --commit SHA: poll majordomus.dev/build.json"]
  d9_V --> d9_G["pages-live gate: served commit is on master, nothing owed past the window"]

The publication path (.github/workflows/pages.yml, modelled in .ai/repo/ci/pages.yaml) does not regenerate: the committed projections are already checked, so publishing renders them. Three facts are kept separate because they fail separately: the build succeeded, the deployment was pushed, and the public site serves the new commit. Only the last is verified against the public endpoint — build.json on majordomus.dev carries the commit it was built from. In the deploy job that verification (scripts/pages verify --commit … --smoke) is advisory: the step may fail without failing the job, and the job reports "pushed" and "serving" as separate facts. What judges is the pages-live gate, run at finish, which checks that the served commit is on master and that no publication has been owed longer than the deploy window (share/standard/majordomus/rules/publication-currency.v1.md).

The version is authored once

The version is authored in one place, the crate manifest's [package] version, and written by one command, majordomus-cli release bump (apps/majordomus-cli/src/release/version.rs), which also keeps the lock's own entry in step and reads both back. The shell tool states no version: it reads share/version.txt, a generated projection of the manifest that ships beside it (ADR 0085), and release::version::diagnose refuses a version written by hand anywhere the tool's files live — the gate version-authored-once. The minimum bump is measured, not chosen: release analyze compares the public capability surface — ids, exposures and schemas, with input and output compatibility judged in opposite directions — against the last release (ADR 0051). Conventional commits are evidence, not the authority. The same analysis is GET /api/v1/release/analysis, an MCP tool and /cockpit/release. After a bump, just derive rewrites share/version.txt, every generator stamp, the changelog, docs/INSTALL.md and the site's version data.

A tag starts the release workflow: build the platform matrix from docs/generated/distribution-matrix.json, publish, write the release record under .ai/repo/releases/ from the real artifact digests, open a pull request with it, and smoke-test the public installer once the record is on the default branch.

What doctor checks

majordomus doctor is read-only and dispatches every doctrine bound to it (lib/doctor.sh). Among them: the vendored rule package against its manifest hashes; the doctrine wiring chain from validator to dispatch to failure propagation to test, claim and CI; provider instruction files against their generation stamps (unstamped or hand-edited fails); the wiring of every enforcement entry in the policy, including the pre-commit derivation check; schema integrity, layout, budgets, retention, the rule DAG, ADRs, skills and prompts. The Rust health.report adds a comparison of the committed registry with an in-process render.

Doctor detects; it does not repair. Regeneration is majordomus generate and just derive; repair of a hand-edited provider projection is majordomus update --diff <target> and then --force — plain update refuses to overwrite a hand edit. A generator is not self-healing merely because it exists: regeneration runs unattended only in the version and release workflows, which open pull requests, and nothing repairs in place.

Circular dogfooding

Majordomus is supervised by Majordomus. The rules that forbid hand-kept registries are rules in this repository's own .ai/; the proof engine that says whether they are enforced reads this repository's gates; the generators that write the reference are checked by the gates those rules name; the evidence ledger can record runs of the tests that prove the generators deterministic (today it holds none of them: jq -r '.executions[].test' .ai/repo/evidence/ledger.json); and the Cockpit renders that proof from the same registry it proves. When a generator breaks, the gate a rule about generators names fails in CI, and the rule itself reads failing only once that run is recorded. The loop is only as strong as its weakest recorded link — today, recording executions — which is why that link is named below.

What is not generated

Majordomus generates mechanical projections. It does not generate human judgment and then quietly call it canonical truth.

Authored by people, never generatedDerived by the machine from it
intent: why a milestone exists, what done meansstatus, waves, readiness
a decision and its trade-offs, in an ADRthe ADR's index entry, links from rules and files
acceptance of a decisionnothing: an extracted ADR may not call itself accepted
the statement of a rule and why it existsits mode, gates, proof state, surfaces
a claim about what the tool guaranteesits evidence state
explanatory prose, including this documentits site page, links, order and description
a handover's objective and next actionits repository, branch, head and changed files

If a person records OAuth must use PKCE, the machine may derive the rules it touches, the context that should carry it, the links from documentation and whether the implementation is tested. It may not rewrite the decision because a heuristic found it unlikely. The inference in the context compiler is capped below a direct declaration for exactly that reason.

Generation matrix

EntityAuthoredDiscoveredDerivedCLIHTTPMCPCockpitWebsite
capabilitycapability!module compositionavailability, visibility, execution, benchmarksevery one through run <id>; a dedicated command where a clap command renders ityes, when exposedtool or resource, when exposedexplorer and runner/registry/ pages
commandclap tree, share/commands.yamljust recipescommand graph, completionyesthrough its capabilitythrough its capability/cockpit/commands/docs/cli/, /commands/
document objectany indexed filesources.yamlURI, kindentity kinds, entity show; shell search; run objects.list/api/v1/objects, /api/v1/entity*resourceobject pageonly kinds with a projection
rulerule fileindexmode, gates, proof staterules report, rules show/api/v1/rulesmajordomus_rules, majordomus_ruleobject page, runner/rules/<id>-<version>/ for every rule in force; /doctrines/ for doctrines
doctrinea rule with a validatordoctrine loaderdoctrine registryshell doctrinevia rulesvia rulesvia rules/doctrines/
claimdocs/CLAIMS.yaml, docs/claims/indexevidence stateevidence claimevidence endpointsevidence toolsobject page, runner/guarantees/
ADRADR fileindexnoneentity showobjectsresourceobject page/adrs/adr-NNNN/, one generated page per decision
issue, milestoneYAMLindexstatus, waves, nextshell plan; the Rust plan capabilities through run plan.<op> only/api/v1/plan/*, POST /api/v1/plan/transitionmajordomus_plan*runner only, no plan page/plan/, roadmap, dependency graph
peer (observed: attachment and announcement)announcementMCP attachoverlapsrun peers.list; a section of shell context; announcing needs an MCP session/api/v1/peers; POST /api/v1/peers/announce exists but refuses without an MCP sessionmajordomus_peers, majordomus_announce/cockpit/peersnone
mesh node (observed: presence), mesh claimmesh declaration; mesh.claimproviders, linkspresence, trust label, folded journal statemajordomus-cli mesh/api/v1/mesh/*majordomus_mesh*/cockpit/meshnone
environmentpolicy, manifestgit, toolchains, servicessnapshot with provenanceenv/api/v1/environmentmajordomus://environmentnot on the overviewnone
design tokenshare/design/tokens.yamlnonestylesheets, logo, design datanonedesign reportdesign tools/cockpit/design, all stylingall styling
versionthe crate manifest's [package] version, written by release bumpthe last release recordthe minimum bumprelease analyze/api/v1/release/analysisrelease tool/cockpit/releasechangelog, install

Architecture debt

Where the repository is not yet the model this document describes, measured on the default branch. The surfaces do not all disclose these gaps themselves; they are listed here so a reader does not have to find them.

Declared twice or registered by hand

  • The command line is a second declaration in clap, checked by closure and parity but not generated; a command path is also repeated as the words its module hands to by_cli, and the helper that does so is copied across several command modules. The text renderers and the runnable examples are written by hand.
  • Adding a capability module edits the pub mod list and the one compose_modules! call.
  • The Cockpit's areas, static routes and dispatcher are hand-written and held equal to each other by tests, not derived; the module comment in cockpit/nav.rs names fewer areas than it serves.
  • majordomus generate's command-line target list is a second enum beside the generator's own and lacks one of its targets, which therefore runs only under all.
  • The shell tool keeps its own dispatch table beside share/commands.yaml, and rules, skills, evidence, knowledge and bench exist in both programs with different meanings.
  • Two allow-lists, share/allow/commands.txt and share/allow/events.txt, are authored; no generator derives them yet.
  • The MCP bridge special-cases the announce tool for reconnect replay.
  • docs/HARDCODING_LEDGER.yaml lists the places a fact is written twice that have been filed, with the command that reproduces each; several open entries remain, and several duplications named in this section are not in it yet. The surface table in docs/WEB.md is one of them in spirit: typed by hand and checked by no test.

Derived, but not faithfully

  • MCP tool annotations: the destructive and idempotent hints are the same for every tool, including the ones that write the repository; only the read-only hint follows the declaration.
  • OpenAPI attaches the 422 refused response to commands only, although some queries refuse.
  • The pipeline diagram on the website's architecture page is a fixed sequence written in the generator, and omits the executable's own generation stages.
  • The doctrine-to-principle relation is ordinary depends_on; there is no typed field. Rule-to-claim links exist on vendored doctrines only; claims do not point back at rules.
  • The members of a collection file (claims, commands, the use-case taxonomy) are indexed without the per-member schema check every other object gets.

Not exposed everywhere

  • No per-kind Cockpit pages for rules, doctrines, ADRs or claims — the generic object page serves them. No Cockpit plan page, and the overview shows the preflight but not the environment snapshot.
  • The peer board has no dedicated command — run peers.list reads it — and announcing needs an MCP session.
  • The shell lifecycle commands (start, check, finish, handover, context) are not capabilities, so they have no HTTP or MCP projection; continuity.state is their read model.
  • Session hooks are adapted for Claude Code only; the other providers declare theirs and are unadapted (share/providers.yaml, lib/capture.sh).
  • Swagger UI's assets come from a CDN.

Not evidenced

  • Executions are recorded in the ledger only by evidence record; CI runs every gate but does not write its runs back, so most rules and claims read not_run rather than proven, and the site publishes the declared and the supported counts side by side to say so.
  • Declarative rules name no proof and stand at unproven; they are advisory, so they are not findings. Reviewed rules are enforced by a person and are not counted as machine-enforced.

Not enforced as documented

  • project.derived-files-regenerated says scripts/derive-check runs in CI and in the Pages workflow; it runs only in the version-bump and release workflows, while the pull-request gate runs its halves separately. The pre-commit half compares the input hash and skips the registry comparison when no executable is built.
  • The serving probe of a deploy is advisory inside the deploy job; once it sees the commit, the published docs and rustdoc are verified and fail the job, as does an errored GitHub Pages build, and the pages-live gate at finish judges whether publication is current.
  • project.no-counts-in-prose is mechanised for the always-loaded provider context (doctor) and the homepage's proof band (homepage-check); for documents it is enforced by review.
  • evidence.record writes the committed ledger but declares the process_state effect, so the effect-derived list of repository writers (MCP instructions, x-majordomus-effect) omits it.

Not built, or partly built

  • Tailscale and mDNS discovery providers; NAT traversal and wide-area links. Cross-machine cooperation exists over rendezvous endpoints and trusted keys, is experimental, and its multi-machine operation rests on a recorded manual run (docs/MESH.md).
  • The Rust side of the session lifecycle is a read model; the cutover is proposed.
  • The intent record is partly adopted (ADR 0070): .ai/repo/project/intents/ holds the records intent validate and intent preflight read; a task is bound to them at start and briefed from them (ADR 0111), and no completion policy consumes them.
  • A version can be declared before it is released: the crate's version and the latest release record are separate facts; the navbar names the release and the changelog shows the unreleased version above it.
  • Many ADRs that the code already follows remain proposed, because acceptance is a person's act.

Claim audit

Each strong statement in this document, with how to check it.

StatementCheck
HTTP, OpenAPI, MCP and the Cockpit runner need no per-capability coderead Router::capability in apps/majordomus-cli/src/http/router.rs and mcp/surface.rs; grep -rn '"rules.show"' apps/majordomus-cli/src finds only the declaration and tests
OpenAPI is derived from the registry that serves requestsapps/majordomus-cli/src/http/openapi.rs; bin/majordomus-cli generate --check
every projection has a declaration and vice versacargo test --test projections in apps/majordomus-cli
the command line is a checked second declarationapps/majordomus-cli/src/capability/closure.rs, apps/majordomus-cli/src/cli/local.rs, scripts/ci/projection-check
a doctrine is a rule with a validatorlib/doctrine.sh; bin/majordomus doctrine list
enforcement mode is derived, not declaredADR 0048; rule.v1.proto has no mode field
a failing test makes a rule failing; otherwise its state is the weakest of the tests that can carry proofapps/majordomus-cli/src/rules/mod.rs, apps/majordomus-cli/src/evidence/freshness.rs; bin/majordomus-cli rules report
issue status is derived with no stored statusan issue YAML has no status key; lib/project.awk; bin/majordomus plan status
overlap is advisorylib/check.sh returns 0 after --overlap; peers.rs documents informational overlaps
the mesh is off by default and has no Tailscale providerls share/skeleton/ai/repo has no mesh/; ls apps/majordomus-cli/src/mesh
this repository runs its own mesh, trusting the keys its declaration lists.ai/repo/mesh/majordomus.yaml; test/cases/491_the_mesh_is_on_here.sh
the board is in memory onlymodule comment of apps/majordomus-cli/src/peers.rs
publication is verified against the public endpointscripts/pages verify; curl -s https://majordomus.dev/build.json
the version is authored in one place and projected for the shell toolapps/majordomus-cli/src/release/version.rs; share/version.txt; scripts/ci/release-check; gate version-authored-once

Twenty-five questions, answered

The questions a technical reader should be able to answer after this document, each with the place that answers it.

QuestionAnswer
Where does a command come from?A clap variant in apps/majordomus-cli/src/cli.rs (Rust) or a case in bin/majordomus with its row in share/commands.yaml (shell); and for every capability, the generic majordomus-cli run <id>. The command line
Is it registered by hand or derived?A dedicated command by hand, as a checked second declaration — closure and parity fail a tree that disagrees with the registry; run <id> is derived. Same section
Where does an HTTP route come from?The capability's HttpExposure; the router asks the registry on every request. HTTP
How is OpenAPI made?Walked from the registry by http/openapi.rs, schemas from the Rust types. OpenAPI
How does Swagger UI relate to the API?It renders /openapi.json and holds nothing of its own. Swagger UI
How does an AI worker get the same capability?As the MCP tool or resource its exposure names, through the same executor. MCP
How does a person see it in the Cockpit?The generic capability page and runner, form from the input schema. The Cockpit
How is Cockpit navigation made?Areas and routes are written; the catalogues inside them are derived. Routes, navigation and order
Where do rule and doctrine pages come from?The index and the doctrine registry, through scripts/generate-site-data: /rules/, /doctrines/, /adrs/; the Cockpit's object page. Documentation and the website
How does a doctrine relate to rules?A doctrine is a rule whose block names a validator; principles are rules it depends on. Governance
How does a rule relate to its validator?Its x-majordomus block names it, and the mode is derived from what is named. Governance, under "A rule's shape"
How does a validator relate to evidence?A run is recorded in the ledger with its commit and digest, and judged against the tree. Evidence
How do you know a rule is enforced?Its rung on the proof ladder and its state in rules report. A rule existing is not a rule being enforced
How is drift between projections detected?Byte comparison of committed against regenerated output, and projection tests both ways. How drift is prevented
How does discovery work?Kinds and source classes are declarations; git's tracked files are matched against them. Discovery
How do typed registries work?The object index for documents, the capability registry for operations, validated whole. The registry
What is canonical, derived, observed, inferred?Six kinds of state
How is a session's context made?Ordered sections fitted to a budget, every drop named. Context for a session
How is a handover made?An authored body with derived front matter, resolved by worktree and branch. Handover
How are execution waves derived?Kahn layering of the issue dependency graph. Planning
How does peer and mesh discovery work?The board through MCP attachment and lease files; nodes through signed datagrams and rendezvous. The peer board, overlap and the mesh
How do mesh data reach the surfaces?Through mesh.* and peers.* capabilities over HTTP, MCP, the Cockpit and run, like everything else. The mesh: who exists
How is a scope collision derived?Scopes compared as equal or nested paths; reported, and refused only for an exclusive mesh claim. Overlap
How does a new capability reach the surfaces?What happens when a capability is added
Where are the exceptions?Architecture debt

Why this matters

flowchart LR
  d10_W["more workers"] --> d10_D["more descriptions of the repository"]
  d10_S["more interfaces"] --> d10_D
  d10_MD["more metadata"] --> d10_D
  d10_D --> d10_X["more drift, faster"]
  d10_ONE["one model"] --> d10_P1["command line"]
  d10_ONE --> d10_P2["MCP"]
  d10_ONE --> d10_P3["HTTP and OpenAPI"]
  d10_ONE --> d10_P4["Cockpit"]
  d10_ONE --> d10_P5["docs and site"]

Majordomus is not a collection of commands, dashboards and agent integrations. It is a repository-native model from which those surfaces are derived.

The command line, the HTTP API, MCP, OpenAPI, Swagger UI, the Cockpit and the documentation are meant to be projections of one typed repository reality — and where one of them is still written by hand, a check holds it to that reality and this document names it.

Rules and doctrines are not valuable because they are written down. They become valuable when their relationship to validators, gates, executions and evidence is machine-inspectable — and when the surfaces say not run rather than a green badge until a run is recorded.

The peer board and the mesh are not agent orchestration. They make workers, their presence and their claims visible enough for people and coordination mechanisms to reason about them.

Generation and derivation are how Majordomus tries to keep one repository from slowly accumulating seven incompatible descriptions of itself.

Searches the titles and descriptions of every page this site publishes.