Add mesh-knowledge, the knowledge base's first slice (hq issue 425, ADR 0290)
mesh/merge-gate pass: builds new: modules/mesh-knowledge, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compo…
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request

An agent learns what a decision says only by reading whole records. This module
holds every decision record and research effort whole in a database in the store,
with clauses that carry their own status and source, typed links read from both
ends, and every version. It answers ask, get and holds within a stated bound, and
takes a record through reserve, propose (refused on any of the 29 per-record
rules), review by someone other than the author, and acceptance, on the
operator's proven warrant when the record needs it. The 4 whole-set rules make
the module unhealthy when they fail. The import reads hq at a commit through the
records module, never overwrites, and changes nothing when run again. Prose
decisions get drafted clauses that a second judgement must confirm before they
are served.
This commit is contained in:
2026-10-11 04:58:35 +02:00
parent 27cb95e98f
commit ba7b455719
50 changed files with 11883 additions and 0 deletions
@@ -0,0 +1 @@
331e8d0bbef401cee83c28aef27db9e823a901c7
Binary file not shown.
@@ -0,0 +1,680 @@
# Glossary — the words of the mesh, by domain, and the ones it stopped using
One name per thing. This page is the authority; where an older record says something else, that
record keeps its words and this page says how to read them. It exists because the words kept drifting
in conversation — "control plane", "controller" and "master" for one thing, "substrate" and
"foundation" for another — and a mesh you cannot name precisely is a mesh two people describe differently. The rule and
its check are [ADR 0244](../02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md);
the domains are drawn in [to-be 49](../03-DESIGN/01-to-be/49-the-mesh-in-domains.md).
## How to read this page
A **domain** is an area of the mesh that owns a set of concepts: inside it each concept has one word,
and the domain decides what that word means. Other domains use the word as it is defined here and never
change its meaning. Every word is defined once, in the domain that owns it; a section lists the words
it uses from other domains under *Uses*, with no second definition.
Two fixed lines follow an entry where they apply, and `00-META/checks/words.py` reads them:
- *Not:* followed by struck-through words — the words this entry replaced. A struck word with no scope
is retired everywhere; *(hq)* retires it in this repository's prose only; *(tools)* in the
descriptions of the mesh's tools and seat verbs only. A retired word may be quoted, never used.
- *Identifier until renamed:* followed by a name in a code span — code that still carries an old name.
It may stand in a code span and nowhere else until the code is renamed, and then the line goes.
Nothing else on this page is struck through. A word that means different things in different domains
is listed under [Homonyms](#homonyms), with the qualified form each domain uses.
## Words every domain uses
- **mesh** — the whole: the nodes, the modules assigned to them, the controller that decides and the
records it keeps. One mesh per operator; a second mesh is a second everything.
- **domain** — an area of the mesh that owns a set of concepts and the one word for each, as described
in the section above ([ADR 0244](../02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md)).
ADR 0006 called seven of them the controller's *contexts*; they carry over as domains under the same
names, except *observability*, which is now **Health and repair**. Where a domain's records live in
the controller, the domain owns that store alone ([ADR 0008](../02-DECISIONS/0008-a-context-owns-its-store.md)).
*Context* in that sense is not used in new writing; the word stays ordinary English, so this is
checked by review, not by `words.py`.
- **machine** — any computer: hardware, a virtual machine, before, during or outside its membership of
the mesh. Prose about the computer itself — its disks, its kernel, the network it sits on, what was on
it before the mesh — says machine.
- **node** — a machine the mesh has adopted and owns. A machine becomes a node when it joins
([ADR 0004](../02-DECISIONS/0004-a-node-and-how-it-joins.md)), and from then on it runs the node-engine
and is either **adopted** (the mesh holds what it found there as found) or **converged** (the mesh made
it what it is) — [ADR 0100](../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md).
Prose about a member of the mesh says node. A node is just a machine that has joined; being one
implies nothing about what it runs.
- **module** — the unit the mesh assigns: one named thing, defined by its manifest, that a node runs —
a service, a container, files, packages, its own code — and the tools it serves
([ADR 0040](../02-DECISIONS/0040-what-a-module-is.md)). Everything configurable on a node is a module.
- **seat** — a named role at a scope (node / site / mesh), held by a module assignment, from a **closed
set** the mesh defines: a claim naming a seat outside the set is refused. A seat may **deliver a
provision**, and its holder is then the mesh's answer for it when several modules provide it
([ADR 0126](../02-DECISIONS/0126-a-module-declares-its-own-seats.md)). The set, with who holds each
seat, is the overview of what a mesh has ([26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md)).
A seat carries **verbs** — the tools every holder must serve. A verb added to a held seat is first
promised as optional, then served, then required ([ADR 0246](../02-DECISIONS/0246-a-seats-new-verb-is-promised-before-it-is-required.md)).
A node seat may also name **the paths its holder owns**, such as `/etc/hosts`: a claim whose module does
not declare them is refused, and only a claimant declares them ([ADR 0287](../02-DECISIONS/0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md)).
- **operator** — the person who runs the mesh, and the one the mesh talks to.
*Not:* ~~master~~ (hq)
- **person** — any human, as against the mesh acting unattended: *a person's word* releases a held
delivery, *a person* deletes a consumer's data. The operator is one; the word says that a human, not
the mesh, acted.
## Module — what a module is and declares
**Purpose.** To say what one module is, in the one document every other domain reads.
**Recorded in** the catalogue, one manifest per module. **Upstream of** every other domain; its words
are a published vocabulary, fixed by the manifest's schema. **Decided by** ADR 0040, 0126, 0174, 0188,
0207, 0236, 0262.
**Uses:** module, seat, provision (Provisioning), data class (Data), health (Health and repair).
- **manifest** — the file a module is defined by (`module.json`): what it is, what it provides and
requires, the seats it claims, the resources it declares, its tools, its data and its health. Say
manifest in prose, not the file's name.
*Not:* ~~module definition~~ (hq)
- **resource** — one thing a manifest declares a node must have: a file, a directory, a service, a
container, a package, a bundle, an archive. A module **declares** resources; the verb *declares* is
this domain's and means what a manifest states.
- **claim** — a module taking a spot on a seat: `claims: [{name, scope}]` in a manifest. A mesh-scoped
exclusive claim is how the mesh says "there is one of me". A foundation seat is named after the role
it guards: the `mesh-controller`, `postgres` and `nats` modules claim the `mesh-controller`,
`mesh-store` and `mesh-broker` seats ([ADR 0079](../02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md),
[ADR 0116](../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md)).
- **setting** — a value a manifest declares and an assignment gives, one of the two ways a node varies
a module ([ADR 0174](../02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)).
What a "flavor" once varied is a setting or a separate module.
*Not:* ~~flavor~~
- **preference** — a setting whose manifest gives it a default, because its value is true of the
software and only tuned per node: a font size, a width
([ADR 0262](../02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md)).
A setting with no default is the operator's own value: a domain, an identity.
*Not:* ~~tunable~~ (hq)
- **kept region** — a marked block in a managed file the mesh writes *into*, where the operator's own
lines survive every send and are given back when the module goes (ADR 0174). The other way a node
varies a module.
- **bundle** — the artifact a module's own code is built into — its tools, a seat's implementation, a
daemon — in any language the mesh has a toolchain for; never an image. A **tools bundle** speaks MCP to
the tool runner. One module may declare several
([ADR 0188](../02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)).
- **tool** — one capability a module serves through the tool runner, addressed `<module>.<tool>`. A
seat's **verb** is the same thing carried by a seat rather than a module.
- **mesh-sdk** — the library a module's own code is written against, including the harness that serves a
tools bundle to the tool runner. The harness is part of it, not a library of its own.
*Not:* ~~tools-sdk~~
- **invokes** — the manifest word for the tools a module calls, `<module>.<tool>` each or `*` for every
one. A grant on the publish side and nothing else; a module that declares none calls nothing.
- **upgrade policy** — how a module's new builds reach its nodes: `roll` (one node first, judged, then
the rest), `together`, or `record` (register the build, send it nowhere)
([ADR 0236](../02-DECISIONS/0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md)).
The controller's verb `upgrade` shows it.
## Core — the mesh's own machinery
**Purpose.** To keep the controller, the bus, the store and every node's engine running and agreed, so
every other domain has something to run on. **Recorded by** the controller and the store.
**Upstream of** every domain but Module; the others use the bus and the store as they are.
**Decided by** ADR 0005, 0006, 0067, 0106, 0141, 0175, 0227, 0229, 0289.
**Uses:** node, module, seat.
- **controller** — the component that decides what each node should be, holds the mesh's records, and
tells nodes over the bus. The relationship is *controller and nodes*, and no node is subordinate: a
node applies its declaration on its own and survives the control-node dying.
*Not:* ~~control plane~~, ~~slave~~ (hq), ~~mesh-control~~ (hq)
- **mesh-controller** — the module that runs the controller. It claims the `mesh-controller` seat at
mesh scope, which is what makes it singular.
- **control-node** — the one node that also holds the `mesh-controller` seat. There is exactly one per
mesh. It is not a separate kind of node — it is a node that additionally runs the controller (and,
today, the foundation). Lose it and the other nodes keep running what they were last told; they simply
cannot be told anything new.
- **node-engine** — the program on every node that applies what the controller declares: it receives
the node's declaration, writes the files, runs the services and containers, and reports what it did.
It is the engine, not a module: it owns no file's content, and every file it writes belongs to the
module that declared it. *Agent* is avoided because that word means a coding agent here. A record
written before the rename keeps the old name. "The host" is retired in this repository's prose only:
in the tools' descriptions it is also an SSH server's or a container runtime's own word, so a
description that means the node-engine by it is found by review.
*Not:* ~~host agent~~, ~~the host~~ (hq), ~~node host~~
*Identifier until renamed:* `mesh-host` — the repository, its binary and its service unit; the rename is
[issue 354](../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md)
- **tool runner** — the one program on each node that loads every assigned module's tools bundle and
serves every tool and every held seat's verb on the subjects the memberships issue; the node-engine
supervises it as a process beside the services it runs, never a container
([ADR 0175](../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md),
which called it "node tools"). Its mode on loopback is the mesh MCP server.
*Not:* ~~node tools~~, ~~tool runtime~~ (hq)
*Identifier until renamed:* `node-tools` — the module, its unit and its bus account; the rename is
[issue 354](../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md)
- **foundation** — the store and the bus, raised at genesis before any module system exists. The
foundation is not a third thing beside the store and the bus — it *is* those two, named together.
*Not:* ~~substrate~~
- **genesis** — raising the foundation and the controller on an empty mesh, by the one installation
done by hand ([ADR 0067](../02-DECISIONS/0067-genesis-is-a-pivot.md)).
- **store** — the one postgres server. It holds the controller's own databases (`inventory`,
`identity`, `licences`, each owned by one domain, ADR 0008) and every module's own database. One server,
many databases — never one shared mesh database. Bare *store* means this and nothing else
(see [Homonyms](#homonyms)).
- **bus** — the mesh's own nervous system: NATS, one per mesh, carrying every link the mesh has —
control, declarations, builds, events, tool calls ([ADR 0106](../02-DECISIONS/0106-the-bus-is-nats.md)).
A module reaches it by requiring `mesh-bus` ([ADR 0128](../02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md));
one that does not require it has no account on it. Held by the `mesh-broker` seat, which is named
after the role rather than the server, so the server can change without the seat doing so. The `nats`
module holds it.
- **the deprecated broker** — the lavinmq module. It was the mesh's bus and is not any more. It keeps
running as an **ordinary provider** of the `amqp` provision, for modules that need a message broker
of their own the way something needs a database
([ADR 0131](../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)) — it claims no
seat, is not foundation, is never raised at genesis, and a mesh that never installs it is complete.
Say *the deprecated broker*, not *the compatibility broker* (it serves the mesh's own modules, not only
the predecessor's) and not *the AMQP broker* (naming it after a protocol invites describing the bus by
contrast with it, which is backwards).
- **layer** — one of the four levels the mesh is built in, from the bottom: the node-engine, the
foundation, the controller, the surfaces. The records' `topic: the tiers` and `repos.md` say *tier* for
this; new prose says layer (see [Homonyms](#homonyms)).
- **lease** and **epoch** — which controller may send (the lease, held in the bus and renewed), and the
order of what it sent (the epoch, which a node reads before it accepts a declaration)
([ADR 0229](../02-DECISIONS/0229-the-cores-order-is-a-lease-the-store-remembers-and-an-epoch-a-machine-is-sent-once-it-reads-one.md)).
- **record mirror** — a key-value bucket on the bus that the controller writes from one kind of its
records (nodes, assignments, walks, builds, settings), after each change commits in its store and only
when the value differs, with one event per write. The store stays the record; the self-check compares
the two and raises `mirror-drifted` on any difference
([ADR 0289](../02-DECISIONS/0289-the-controller-mirrors-its-records-into-buckets-written-only-on-a-change-and-audited-against-its-store.md)). Shortened to *mirror* where the
text is about the controller's records; the builder's *mirrored* `upstream` image (Change and delivery) is
a different thing, a copy of an outside image in the artifact store.
## Placement — what runs on which node
**Purpose.** To decide, send and apply what each node runs, and to say why.
**Recorded by** the controller's `inventory` database. **Upstream of** Provisioning, Connectivity, Data
and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 0252, 0261.
**Uses:** node, machine, module, seat, setting (Module), manifest (Module).
- **assignment** — a module put on a node, with the settings that node gives it.
- **pending assignment** — an assignment of a module whose build is still running, kept, and made by the
controller when that build registers the module, or ended with why when it does not
([ADR 0261](../02-DECISIONS/0261-an-assignment-made-while-its-modules-build-runs-is-kept-pending-and-the-controller-makes-it.md)).
- **scope** — where a seat or a claim holds: `node`, `site` or `mesh`. `node` is also the scope's
value in a manifest.
- **capacity** — how many holders a seat takes. A capacity-1 seat is exclusive. A higher-capacity seat
is a **bench**: several holders coexist. A **replicated** bench has one holder per node, each on record
and each answering the same, like `mesh-dns-resolver`
([ADR 0223](../02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md)). A
**kinded** bench has holders that are different modules, each claiming one **kind**, and a verb's
subject carries the kind; `channel` and `intake` are the only ones (ADR 0234).
- **installed / holding** — a module may be assigned (its package installed, its files placed) without
holding the seat its family declares; *holding* is being the one — the login shell, the display session
— on that node ([ADR 0176](../02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)).
The module that holds a seat is its **holder**.
- **depends on a seat** — a module needing a seat held on its node by some module, without holding it.
Derived from the resources it declares, never stated: a `service` depends on `node-service-manager`, a
`package` on `node-package-manager`, a `container` on `node-container-runtime`
([ADR 0207](../02-DECISIONS/0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md)).
Not a claim: a module claims a seat it holds and declares resources.
- **declaration** — what the controller sends one node: every resource of every module assigned there,
composed. The noun is this domain's; what a manifest says is that it *declares* (Module), and a manifest
is never called a declaration. The controller's verb `plan` previews a node's declaration.
- **send** — the controller giving a node its declaration. The controller's verb `push` asks for a
send (of one node, or of every node); prose says send. A git push and a phone's push notification are
other things (see [Homonyms](#homonyms)).
- **apply** — the node-engine making its node match its declaration; **reconcile** is doing so again
until nothing differs.
- **operator account** — the login name of the person who works on a node, stated on the node record;
empty for a node nobody logs into. Everything the mesh places under a person's home is resolved
against this account's home and owned by it
([ADR 0181](../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)).
Not a name a manifest carries.
- **agent account** — the login every coding agent on a node runs as, when the node names one of its own;
stated on the node record at the controller's terminal, never by a verb or a setting. Empty means agents
run as the operator account. It cannot become root without a person, and the node-engine judges that on
every look ([ADR 0266](../02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md)).
- **account group** — a group of the machine's group database that a module puts an account in, by
declaring the account with that group and nothing else of it. Several modules may each add one to the
same account; the mesh takes back only an account group it added
([ADR 0252](../02-DECISIONS/0252-a-module-puts-an-account-in-a-group-and-the-mesh-says-when-a-new-login-is-needed.md)).
Never *membership*, which is the bus's word.
## Provisioning — one module serving another
**Purpose.** To resolve which module serves a provision for which consumer, and to wire the two.
**Recorded by** the controller. **Upstream of** Data; downstream of Module, Placement and Identity.
**Decided by** ADR 0027, 0084, 0138, 0225, 0230, 0263.
**Uses:** module, seat, credential (Identity and access).
- **provision** — a service one module `provides` and others `require`; the mesh resolves a provider
and wires the two with an endpoint and a credential. A provision is a service you offer, a seat is a
role you occupy, and the two meet where a seat delivers a provision: occupying the seat is what makes
a module *the* provider of it.
- **provider** and **consumer** — the module that provides a provision, and the module that requires
it. Which of several providers serves a consumer is resolved
([ADR 0084](../02-DECISIONS/0084-which-provider-serves-a-consumer.md)).
- **pin** — a person's choice of provider for a consumer, which resolution then respects.
- **endpoint** — where a consumer reaches its provider; an assignment binds it and says how far it
reaches ([ADR 0138](../02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md)).
- **retire** — a provider stops serving a consumer the mesh no longer asks for; what it kept is deleted
only by a person ([ADR 0230](../02-DECISIONS/0230-a-consumer-the-mesh-stops-asking-for-is-retired-and-deleted-only-by-a-person.md)).
- **share** — a folder one node offers to the mesh's other nodes over the private network: the provision
`nfs-share`, provided by the holder of the node seat `node-nfs-server`, one share per folder
([ADR 0263](../02-DECISIONS/0263-a-share-is-a-provision-a-node-mounts-it-on-demand-and-only-nodes-that-ask-get-it.md)).
- **mount** — a share, or an occasional source, made visible at a path on one node by the holder of the
node seat `node-mounts`, as a mount unit and an automount unit, mounted on access (ADR 0263).
- **occasional source** — a share on a device that is not a node and comes and goes, such as a USB disk;
its absence is a state, *not present*, never a fault (ADR 0263).
## Identity and access — who may do what
**Purpose.** To issue, hold, rotate and check the credentials and grants by which modules, nodes, agents
and the operator act. **Recorded by** the controller's `identity` and `licences` databases and the vault.
**Upstream of** Provisioning, Change and delivery, and Operator and conversation.
**Decided by** ADR 0085, 0113, 0183, 0225, 0234.
**Uses:** module, node, operator, consumer (Provisioning).
- **credential** — anything that proves an identity: a password, a key, a token. A **pair credential**
is the two ends of one credential, the consumer's and the provider's.
- **secret** — a credential or other value the vault keeps sealed and the node-engine unseals into a
file at apply ([ADR 0085](../02-DECISIONS/0085-a-secret-is-a-provision.md)).
- **secret family** — a module's own secrets declared as one name ending in `-*`; each member is the prefix
and a name (`smb-password-<source>`, one per source the module's settings name), given by its full name
and never made by the mesh
([ADR 0283](../02-DECISIONS/0283-a-part-that-waits-for-the-operators-secret-or-setting-is-said-as-waiting-passes-the-first-node-gate-and-names-the-act.md)).
- **vault** — the module that owns every secret (`mesh-vault`,
[24 — The secrets vault](../03-DESIGN/01-to-be/24-the-secrets-vault.md)).
- **grant** — what an identity may call or reach, bounded by the provisions it requires
([ADR 0225](../02-DECISIONS/0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md)).
- **bus account** and **membership** — a module's or a node's account on the bus, and the subjects it
is issued. The bus server calls its accounts *users*; the mesh says bus account.
- **rotate** — replacing a credential without a consumer holding one the provider does not know.
- **licence** — a model-access account the licence manager holds and **binds** to a consumer, the
coding agent on a node ([ADR 0183](../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)).
Taking a consumer off a licence is **unbinding** it; the licence manager's verb for it is still
`release`, an identifier to rename.
- **proof** — what an authorising answer carries: a verified sender, a one-time code, a security key's
touch (ADR 0234). How much proof a kind of ask needs is its **assurance level**.
- **controller's terminal** — a command line the controller runs as typed by its operator, not asked through a
verb, so a change that is the terminal's alone is taken (issue 339, issue 340) — or, since
[ADR 0277](../02-DECISIONS/0277-a-trusted-setting-is-proposed-through-a-verb-and-set-on-the-operators-warrant-and-a-modules-own-secret-is-asked-for-and-typed-at-the-desk.md),
proposed through a verb and set on the operator's warrant. In the controller's code it is a
line run without `MESH_VERB`. The operator reaches it with `mesh-cli` from their own account on a node where no
agent can run as that account: the control-node, and a node whose agents run under an account of their own
([ADR 0272](../02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md)).
## Change and delivery — a commit on its way to the nodes
**Purpose.** To take one commit from its pull request to every node that should run it, and to put it
back when it fails. **Recorded by** the `mesh-delivery` module (deliveries) and the controller (the
planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
**Decided by** ADR 0162, 0218, 0236, 0237, 0238, 0239, 0250, 0267, 0270, 0276.
**Uses:** module, node, declaration and send (Placement), health (Health and repair), person.
- **merge check** — the two statuses a pull request carries before it may merge: the **merge gate**
(`mesh/merge-gate`, the build seat's composed check of the modules the change touches) and the
**repository check** (`mesh/repo-check`, the repository's own `merge-check.sh`)
([ADR 0238](../02-DECISIONS/0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md)).
- **build seat** — the seat whose holder builds modules, `node-build-agent`; its holder on a node is the
**builder**. Any node may hold the seat, so there is no build node by nature
([ADR 0237](../02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md)).
*Not:* ~~build machine~~
- **build queue** — the controller's queue of builds to run; one entry in it is a **build request**.
The queue's verbs still say *ask* for an entry; the conversation's ask is another thing
(see [Homonyms](#homonyms)).
- **package** — what code resolves when it is **compiled**: an npm/cargo/pypi dependency, by **version**.
Served by the **package-registry** (gitea). Only a builder talks to it.
- **artifact** — anything a build produces and the mesh delivers to a node by **digest**: an `image`, a
mirrored `upstream` image, a `bundle` of the module's own code, an `archive`. Served by the
**artifact store**, an OCI registry that holds every kind as content-addressed blobs
([ADR 0156](../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)).
An image is one kind of artifact, and a module is not an image. A package and an artifact are two
protocols, not one store being weak ([ADR 0075](../02-DECISIONS/0075-two-stores-and-which-provides-what.md)).
- **build source** — the files one artifact is built from, in one or more repositories: for a Go program
its import closure (the directories of its packages, its Go module files) and its recipe; for any other
artifact the paths its manifest names, by default its module's own directory. Each artifact of a build
has its own, and a change rebuilds the artifacts whose build source it touches
([ADR 0267](../02-DECISIONS/0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md)).
Older records (ADR 0015, ADR 0111) and designs say *build source* for the repository a module
is built from; this entry narrows it to the files within, and that use reads as *the repository holding
the build source*. Sharing a repository is not a dependency, and no build source is a whole repository
holding another module's files.
- **catalogue** — what the mesh holds of every module, at which commit; and the repository the
modules live in. `mesh-catalog` and `catalog_modules` are identifiers and keep their spelling.
*Not:* ~~catalog~~ (hq)
- **delivery** — one commit in one repository on its way to the nodes, from its pull request's head
being announced to one of its final states: one pull request, one status, one note
([ADR 0239](../02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)).
From its merge on it is delivered by the walk of the batch that answers it, and ends with that walk
([ADR 0276](../02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md)).
Its states are one table (`proposed`, `checked`, `ready` or `rejected`, `published`, `delivering`,
`held`, and the final six: `delivered`, `failed`, `superseded`, `stopped`, `retired` and `covered`;
[ADR 0250](../02-DECISIONS/0250-a-merge-heard-only-from-the-buss-history-is-retired-and-owed-work-the-forge-cannot-do-ends.md),
[ADR 0270](../02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md)). Not *change* (a diff). *Pipeline* is the predecessor's build-and-deploy
mechanism, which the as-is designs describe; it is never a word for a delivery.
*Not:* ~~deployment~~ (hq)
- **covered** — the end of a delivery that a later merge containing it delivered: every module it moves was
sent to every machine running it by the walk of a later merge into the same trunk, delivered on its own
passing check, whose merge commit the forge says contains this one. Not *superseded*, which is a delivery
whose walk a newer delivery took over before it ended; nor *delivered*, which its own walk or an empty plan
ends in ([ADR 0270](../02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md)).
- **delivery group** — two or more deliveries sharing a pull request head branch name across the mesh's
repositories, delivered as one unit in an order declared (`after:`) or inferred by the planner. One
level: a group holds deliveries, never groups. Its state is derived from its members, never set.
- **delivery plan** — what a delivery does to the mesh: its **build plan** (the modules it moves and
their dependents, in tiers), its **deploy plan** (per node, what it receives and what waits for a
person) and its verdict (the composed nodes, the replays). Computed by the controller's planner from a
diffset; the controller's verb `delivery-plan` shows it. *Deploy* is used in this one place.
*Not:* ~~change plan~~, ~~release plan~~
- **merge window** — the rolling wait in which the controller collects merges before it plans them: each
merge restarts it, and it closes when none arrived for its length (90 seconds by default) or at the latest
10 minutes after its first merge ([ADR 0276](../02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md)).
- **batch** — the merges one merge window collected, planned once as one walk. For a repository merged more
than once in it, the walk takes the latest commit, and the earlier merges are answered by it. Not *group*,
which is a delivery group.
- **mesh-delivery** — the module that owns deliveries and delivery groups, holding the mesh-scoped seat
of the same name. It records and decides; the controller sends, judges and rolls back when it is asked.
- **walk** — the controller's sending of one batch's builds across nodes, at most one trunk commit per
repository, tier by tier, one node first and judged at the first-node gate, then the rest (ADR 0236,
[ADR 0276](../02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md)). One walk is open at a time; it names every merge it answers. A primitive the delivering stage
asks for, not an object anyone manages. A **tier** is one step of a walk: a module, then the modules
built against it. The controller's verb `plans` lists the walks. Walking is not a noun of its own:
a module *rolls out* by its `roll` policy.
*Not:* ~~rollout~~ (hq)
- **first-node gate** — the judgement on a delivery's first node: healthy three times, no witness put it
back, no condition raised since the send (ADR 0236 §2). Not *the gate* bare (see [Homonyms](#homonyms)).
- **core module** and **leaf module** — a core module is one on the controller's own path (the controller,
the node-engine, the tool runner, the bus, mesh-delivery) or the mesh's resolver, whose judgement protects
the mesh's ability to reach a node at all; a leaf module is every other module. Decided by
[ADR 0282](../02-DECISIONS/0282-a-fix-to-a-leaf-module-runs-healthy-on-every-node-within-five-minutes-of-its-merge-and-a-core-module-within-ten.md).
Not a *core component* (ADR 0236's controller, node-engine, tool runner and bus, each with a health probe):
the core modules add mesh-delivery and the resolver.
- **delivery time** — from a delivery's merge to every node of its deploy plan that is heard from running its
modules' builds and reporting them healthy. Its **delivery budget** is what it is held to: five minutes for
a leaf module, ten for a core module (ADR 0282). Not the walk's time, which ends at the send.
- **release** — a person's word that a held delivery goes on; the verb `mesh-delivery.release`. Used in
this sense only.
- **rollback** — putting a node back on the build it ran before, by something other than the build being
judged (a witness, ADR 0236).
- **the lab**, **scenario**, **replay** — a real mesh raised to run a change against before it reaches
nodes; what a lab run declares; and a recorded failure run again to prove a fix (ADR 0016, ADR 0237).
## Health and repair — what is wrong, and putting it right
**Purpose.** To notice what is wrong with the mesh, say it once, repair what may be repaired unattended,
and record what was done by hand. **Recorded by** the controller's condition store.
**Upstream of** Change and delivery (the first-node gate) and Operator and conversation (a condition
becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252, 0283.
**Uses:** node, module, node-engine (Core).
- **health** — a module's or a core component's statement of how it is alive and ready, which the
node-engine judges ([ADR 0240](../02-DECISIONS/0240-a-module-says-how-it-is-healthy-and-the-node-engine-judges-it.md)).
- **relogin needed** — an account's health when the group database lists it in an account group and its
running session, started before, does not have that group yet. Said by the node-engine as the module's
resource of kind `account`; it clears at the first look after a new login (ADR 0252).
- **waiting** — a resource's health when its module's tool check says it waits for the operator: a named own
secret or setting of the module, not given yet, without which a part cannot work. Not a fault: the
controller checks it and says it as the condition `needs-operator`, naming the act, and the first-node gate
reads it as *waits for a person*
([ADR 0283](../02-DECISIONS/0283-a-part-that-waits-for-the-operators-secret-or-setting-is-said-as-waiting-passes-the-first-node-gate-and-names-the-act.md)).
- **probe** — one look at one thing's health, run by whoever owns the verdict; what a probe returns is a
**finding**, before it becomes a condition.
- **signal** and **watchdog** — something that must keep happening (a heartbeat, a report after a send),
and the watcher that raises a condition when it stops (to-be 45 §3).
- **condition** — one open fact about something the mesh owns that is wrong, with a key and a severity;
raised and cleared only by observation
([ADR 0231](../02-DECISIONS/0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md)).
What a wrapped dashboard raises is its own; the mesh's notion is a condition.
*Not:* ~~alert~~ (hq)
- **self-check** — the controller's own examination of the mesh, run on demand and on a schedule
(to-be 45 §4). Its verb is `doctor`, an identifier; prose says self-check.
*Not:* ~~doctor~~ (hq)
- **healer** — a registered response to one condition kind: its **repair** (the ordinary path again),
its **budget**, its back-off and its brake. A healer may not withdraw, delete or recreate data.
- **drill** — something broken on purpose to see the mesh raise and clear its condition; a drill is
never counted as a repair.
- **rehearsal** — a question the operator starts at the controller's terminal (`rehearse`) and answers on a
channel, to see an answer travel the whole way: its approval performs nothing and is recorded as the
operator's decision ([ADR 0259](../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md)).
Not a drill: nothing is broken.
- **hand-act** — a repair a person made by hand, recorded so the mesh knows it happened.
- **witness** — what rolls a core component back when its new build does not become healthy: never the
component itself (to-be 45 §8).
## Data — what the mesh keeps, and getting it back
**Purpose.** To know every item of data a module holds, how precious it is, and to keep it recoverable.
**Recorded by** the manifests' `data` declarations and every node's `node-backup` seat.
**Upstream of** Change and delivery and Health and repair. **Decided by** ADR 0232, 0233, 0235.
**Uses:** module, node, provider and consumer (Provisioning), person.
- **data class** — how precious an item of data is, ranked by the operator: `irreplaceable`,
`valuable`, `rebuildable`, `cache`; and `none` for a provision that keeps nothing of anybody's
([ADR 0233](../02-DECISIONS/0233-a-module-declares-the-data-it-holds-and-the-mesh-protects-and-watches-it-from-that.md)).
- **backup** — a copy kept elsewhere by the node's `node-backup` holder; **restore point** — one backup
at one moment; **restore** — bringing it back beside the live data, never over it.
- **stream snapshot** — the bus's own backup of each stream, taken by the module holding `mesh-broker`
([ADR 0235](../02-DECISIONS/0235-the-bus-is-backed-up-by-its-own-snapshot-of-each-stream.md)).
- **sticky binding** — a consumer's tie to the data a provider keeps for it, which moves only by a
person ([ADR 0232](../02-DECISIONS/0232-a-binding-to-a-consumers-data-moves-only-by-a-person.md)).
## Connectivity — how nodes and modules reach one another
**Purpose.** To make every node and module reachable by name where it should be, and unreachable where
it should not. **Recorded by** the controller. **Upstream of** Provisioning and Health and repair.
**Decided by** ADR 0007, 0117, 0138, 0223, 0226, 0247.
**Uses:** node, machine, assignment and endpoint.
- **private network** — the mesh's own encrypted network between its nodes, on which every node has an
address and a mesh name ([ADR 0226](../02-DECISIONS/0226-the-private-network-is-assigned-by-its-own-name-and-the-proxy-names-its-public-issuer.md)).
The network the machine sits on without it is the **underlay**.
*Not:* ~~overlay~~ (hq)
- **anchor** and **hub** — the roles a node plays for the private network: the anchor is reachable from
outside and every node reaches it; a hub relays for nodes that cannot reach each other directly.
- **resolver** — a seat holder that answers the mesh's names; a node lists only the mesh's resolvers
(ADR 0223), or, where it holds one, its own resolver, which asks them. **uplink** — a node's
connection to the outside network, a seat ([ADR 0117](../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md)). **hostname** — a node's own name,
a seat.
- **split DNS** — resolving names by domain on one machine: a VPN's domains through the VPN's servers
over its link, every other name through the mesh's resolvers. The provision `split-dns`, provided by
the holder of the node seat `node-resolver`, the machine's **own resolver**, which exists only where
something requires it ([ADR 0247](../02-DECISIONS/0247-a-machine-with-a-vpn-client-routes-names-by-domain-through-a-resolver-of-its-own.md)).
- **proxy** and **public name** — the module that answers a public name and forwards it to an
endpoint on the private network.
- **packet filter** — what the mesh enforces on a node about which packets pass, the
`node-packet-filter` seat and its verb `rules`. A **found firewall** is a program the mesh found on a
machine and keeps retired or in force; *firewall* says that, and a wrapped program's firewall is its own.
- **intrusion prevention** and **ban** — refusing a source that misbehaved, and the refusal itself.
- **reach** — how far an assignment's endpoint may be reached from: the node, the private network, or
the outside (ADR 0138).
## Operator and conversation — the person the mesh works for
**Purpose.** To let the mesh and its operator talk: tell, ask, answer, and act only on an answer it can
trust. **Recorded by** the router (asks, warrants and the operator's identities).
**Downstream of** Health and repair, Change and delivery, and Identity and access. An authorising
answer acts in whichever domain asked, through that domain's own verb; the conversation owns the
asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272.
**Uses:** operator, person, proof (Identity and access), release (Change and delivery).
- **mesh MCP server** — the endpoint on a node's loopback through which every agent and person on that
node reaches the mesh: the MCP server named `mesh`, with its five tools `mesh_overview`,
`mesh_machine`, `mesh_search`, `mesh_describe` and `mesh_call`. It is the tool runner's loopback mode,
not a module of its own. "Console" suggested a terminal or a shell, and the module `mesh-console` it
once named no longer exists; "the tool bridge" and "the brain" named the predecessor's program.
*Not:* ~~console~~ (hq), ~~mesh-console~~, ~~tool bridge~~
- **mesh-cli** — the module on every node that places the operator's command, and the command itself:
`/usr/local/bin/mesh-cli <command line>` asks the controller to run one of its command lines. It asks the
node-engine on its own machine, which says to the controller which account is asking; the controller runs the
line as the **controller's terminal** or as an ordinary call
([ADR 0272](../02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md)).
- **nox** — `mesh-cli`'s alias, in interactive zsh only. A script, `sudo` and a document meant to work everywhere
say `mesh-cli`. The operator also calls the mesh itself *nox mesh*; on this page and in the records *nox* is the
alias alone. It is the one command the operator types: every line the mesh addresses to the operator — a
condition's words, a verb's answer, a README, a design — names a `nox` line, never a daemon's (the controller's
or the node-engine's binary followed by a command), which `words.py` and the catalogue's check refuse
([ADR 0272](../02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md),
its insight of 2026-10-09).
- **channel / intake** — the two kinded benches the mesh talks to its operator through: `channel`
sends, `intake` turns what arrives into one envelope. A holder of either declares **capabilities** from
the fixed vocabulary `channel-capabilities/1`. Not *notifier* (that is the desktop's node seat, one
holder of kind `desktop`) and not *bot* (that is one service's account).
- **router** — the module that holds the `operator-channel` seat, orders the channels and checks every
sender ([ADR 0234](../02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md)).
- **headline / verdict / explanation / resolved line** — what a condition says to the operator in plain
words: a few words naming the thing and what is wrong; "Nothing for you to do." or "Needs you:" and the
one thing the operator can do themselves (its **needs**); one or two sentences on what happened and what
it means; the line said when it clears. Its **actions** are the answers a notification offers. Carried by
the condition beside its **summary**, which keeps the plans, commits and verbs for whoever looks closer
([ADR 0253](../02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md)).
The router may override "Nothing for you to do." in what it says, from the **tracking line** (ADR 0285).
- **ask** — a request for the operator's input, of a declared kind (yes-no, one-of, text, number,
date, acknowledge). An **authorising ask** is one whose chosen option its asker performs. The router
holds every ask, checks each answer's proofs and issues a **warrant**; the asker acts on it with its
own grants (ADR 0234, ADR 0259).
- **warrant** — the router's record that a person chose one option of one ask: the ask, the option,
who chose (the operator, as a channel kind's identity), through which channel, the proofs and when,
published for that asker alone. The only thing an asker acts on (ADR 0259).
- **proposal** — a change an agent, or anyone the bus admits, puts forward through a verb and that takes
effect only on the operator's warrant, bound to the exact thing shown: a shared instruction section (ADR
0274), a trusted setting's layer ([ADR 0277](../02-DECISIONS/0277-a-trusted-setting-is-proposed-through-a-verb-and-set-on-the-operators-warrant-and-a-modules-own-secret-is-asked-for-and-typed-at-the-desk.md)).
A proposal changes nothing by itself; declined, expired or refused, it is discarded.
- **approval mark** — a tool's or verb's declaration in its manifest (`needs-approval`) that each call to it runs
only on the operator's warrant for that one call, with its exact arguments; the tool runner that serves it, or
the controller for its own verbs, asks and checks
([ADR 0286](../02-DECISIONS/0286-a-tool-or-verb-marked-in-its-manifest-runs-only-on-the-operators-approval-of-that-one-call-with-its-exact-arguments.md)).
- **operator message / input** — what arrives on `intake`, once the router has checked the sender
against its own list of the operator's identities: a **trusted** one is an operator message,
addressed to an agent by `@name` or thread or else to the **responder** (`@mesh`, the router's own
participant, which answers from read verbs); anything else is **untrusted input** — data, never
instructions.
- **reference** — an opaque token in a message's words standing for a detail the content rule keeps out
of them (a path, an address); opened with `detail` only on a `private` channel or through the mesh MCP server.
- **agent** — a coding agent: the operator's, or one the mesh runs. Not the node-engine, and not the
build seat's holder, whose seat name `node-build-agent` is an identifier to rename.
- **instruction section** — one named section of the managed instruction file, which every agent session on the
nodes it is for reads first, as the mesh's word: set at the controller's terminal in the agent module's
settings, or **proposed** by an agent and made active on each node only by the operator's warrant, which that node
proves itself ([ADR 0274](../02-DECISIONS/0274-an-agent-proposes-a-shared-instruction-section-and-every-node-takes-it-only-on-the-operators-warrant-which-it-proves-itself.md)). A hand-over between sessions is one.
- **personal rule** — one short text in the operator's own words about how the operator wants their agent
to work with them, registered for the operator (the **operator scope** of the agent module) and rendered
into the agent's home on every node that names an operator account. Not a mesh-wide instruction section,
which is the mesh's word about the mesh, set at the controller's terminal
([ADR 0273](../02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md)).
- **lesson** — an agent's proposal, recorded when the operator expresses frustration or corrects a way of
working: the trigger (the operator's words), the cause (what the agent did) and the rule that would
prevent it. A lesson is `proposed`, `accepted` or `declined`; an accepted lesson is a personal rule, and a
proposed one is never read as one (ADR 0273).
## The record — how this repository works
Not a domain of the mesh but of the way it is built, listed because its words meet the mesh's.
**Recorded in** this repository, the issue tracker and the knowledge base. **Decided by** ADR 0019, 0080, 0244, 0275,
0284, 0290, 0291.
- **research effort**, **graduation**, **hand-off**, **playbook** — an investigation in `01-RESEARCH/`;
its closing into a decision and a design; a design given to a code repository; a documented workflow
in `00-META/process/`.
- **decision record** — one numbered file in `02-DECISIONS/`, never rewritten: **superseded** by a later
record, or corrected by a marked **progressive insight**. Always qualified in this repository: bare
*record* has other meanings (see [Homonyms](#homonyms)).
- **clause** — one numbered statement a decision record makes, with its own status (`proposed`, `in force`,
`changed`, `superseded`): a later record may change one clause and leave the rest
([ADR 0290](../02-DECISIONS/0290-a-decision-record-lives-whole-in-the-knowledge-base-made-of-clauses.md)). Not a *rule*, which is a line of
`00-META/how-we-build.md`.
- **knowledge base** — the role that holds the decision records and research efforts whole (number, status, topic,
typed links, clauses and reasoning, with every version), takes them as proposals, has them reviewed and accepted,
and answers what holds: the seat `knowledge-base`, held by the module `mesh-knowledge`
([55 — The knowledge base](../03-DESIGN/01-to-be/55-the-knowledge-base.md)). Not **the record**, the literal reader
of this repository.
- **design** — a document in `03-DESIGN/`: **as-is** (what runs) or **to-be** (what is being built).
- **issue** — a question about the mesh that someone must answer, numbered and tracked by the issue tracker, which
holds it whole: its report, diagnosis and resolution included ([ADR 0291](../02-DECISIONS/0291-an-issue-lives-whole-in-the-issue-tracker.md));
the folders in `04-ISSUES/` are imported there and removed.
An **incident** is a past event that taught a rule.
- **issue tracker** — the role that holds the mesh's issues and hands out their numbers: the seat `issue-tracker`,
held by the module `mesh-issues` ([53 — The issue tracker](../03-DESIGN/01-to-be/53-the-issue-tracker.md)).
Not hits's *item*; bare *ticket* is a homonym (see [Homonyms](#homonyms)).
*Not:* ~~ticketing system~~ (hq), ~~ticket system~~ (hq)
- **duplicate check** — the issue tracker's judgement, when an issue is opened, of whether it asks a question an
older issue not resolved already asks; a certain duplicate is closed `wontfix` with its report kept on the older
issue ([ADR 0278](../02-DECISIONS/0278-the-issue-tracker-checks-every-new-issue-for-a-duplicate-through-a-judgement-the-agent-makes-without-tools.md)).
Its pass over the backlog is the verb `groom`.
- **triage** — the issue tracker's judgement of an opened issue, made by the agent with no tools: whether it is a
duplicate (the **duplicate check**), its **severity** and what it **needs**, a suggested owner, and the issues it
depends on or relates to; it fills only what nobody gave ([ADR 0279](../02-DECISIONS/0279-the-issue-tracker-triages-every-new-issue-and-an-issue-is-ready-when-what-it-depends-on-is-delivered.md)).
- **ready** — of an issue: every issue it depends on is wontfix, or resolved with its fix delivered.
- **worker** — of an issue: the agent or person, with its node (who@node), that moved it to `diagnosing` or `fixing`
and works on it; another is refused while the worker is current, unless it gives a reason to **take it over**. A
worker with nothing done on the issue for a day is **stale**. **Fixing** is the status an issue is at while its fix
is worked on; an issue is resolved only from it ([ADR 0284](../02-DECISIONS/0284-an-agent-moves-an-issue-to-fixing-under-its-own-name-and-an-issue-is-resolved-only-from-fixing.md)).
Not a *claim* or a *holder*, which are a module's on a seat.
- **tracks** — of an issue and a condition: the issue links the condition's key with a link of kind `condition`. The
**tracking line** is the line the router says in every message about a condition: the issues that track it and
where each stands, or that none does ([ADR 0285](../02-DECISIONS/0285-an-issue-tracks-a-condition-by-its-key-and-every-message-about-the-condition-says-the-issue-and-where-it-stands.md)).
- **hq check** — one of `00-META/checks/`, run by `merge-check.sh` as this repository's check.
## Homonyms
A word below means different things in different domains. In a governing document it is never bare;
it is always the qualified form. **Checked by review, not by `words.py`:** a word list cannot tell one
sense from another, so the reviewer looks for the bare word in the diff. Where a homonym is settled by
renaming one sense, the old sense moves to a *Not:* line and becomes mechanical.
| Word | Sense | Say | Domain |
|---|---|---|---|
| plan | what the controller would send one node | that node's **declaration** (verb `plan`) | Placement |
| | what a delivery would do to the mesh | **delivery plan** | Change and delivery |
| | the sending of one batch of commits across nodes | **walk** (verb `plans`) | Change and delivery |
| | a step a person starts, like the bus's | **planned step** | Core |
| membership | the subjects a bus account is issued | **membership** | Provisioning |
| | an account in a group of the machine | **account group** | Placement |
| push | the controller giving nodes their declarations | **send** (verb `push`) | Placement |
| | a git push | **git push** | The record |
| | a phone notification | **push notification** | Operator and conversation |
| release | a person letting a held delivery go on | **release** | Change and delivery |
| | taking a consumer off a licence | **unbind** (verb `release`, to rename) | Identity and access |
| gate | the judgement on a delivery's first node | **first-node gate** | Change and delivery |
| | the pull request status | **merge gate** | Change and delivery |
| | a failed step stopping its module's later steps (ADR 0136) | *a failed step holds its module* | Placement |
| tier | a level of the mesh | **layer** | Core |
| | a step of a walk | **tier** | Change and delivery |
| | how much proof an ask needs | **assurance level** | Identity and access |
| ask | a request for the operator's input | **ask** | Operator and conversation |
| | an entry in the build queue | **build request** | Change and delivery |
| store | the one database server | **store** | Core |
| | the OCI registry | **artifact store** | Change and delivery |
| | a node's own copy of its last declaration | **last declaration** | Placement |
| record | a numbered decision | **decision record** | The record |
| | the literal reader of this repository, searched first for a symptom | **the record** (the `records` module) | Operator and conversation |
| | the upgrade policy that sends nowhere | `record` | Module |
| knowledge base | the module holding the decision records and research efforts whole | **knowledge base** | The record |
| | the predecessor's operational memory, in older playbooks (issue 430) | none: it no longer exists | — |
| check | a pull request's two statuses | **merge check**, **merge gate**, **repository check** | Change and delivery |
| | a delivery group's verdict | **composed check** | Change and delivery |
| | the controller's own examination | **self-check** | Health and repair |
| | a file in `00-META/checks/` | **hq check** | The record |
| agent | a coding agent | **agent** | Operator and conversation |
| | the build seat's holder | **builder** | Change and delivery |
| the user | an account of a wrapped program, or of the bus server | **its account**, **bus account** | Identity and access |
| | a human | **operator** or **person** | words every domain uses |
| firewall | what the mesh enforces | **packet filter** | Connectivity |
| | what was found on a machine | **found firewall** | Connectivity |
| ticket | an answer's one-time token (ADR 0258) | **answer ticket** | Operator and conversation |
| | a question someone must answer | **issue** | The record |
| deploy | the per-node part of a delivery plan | **deploy plan** | Change and delivery |
| | a wrapped program's own deploy | its own word | — |
## How this page is kept
A new word for an existing thing lands here first, in the domain that owns it, in the same change that
introduces it in code. A word moves to another domain only with a decision record. A retired word goes
on a *Not:* line and nowhere else on this page, and `words.py` then fails on it in running prose. A
record under `02-DECISIONS/` keeps whatever word it was written with — those are immutable — so a word
retired here may still appear there, and the *Not:* lines are how to read it.
**How it is checked** ([ADR 0244](../02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md)):
`python3 00-META/checks/words.py`, run by `merge-check.sh` on every pull request, fails on a retired
word or a bare identifier in running prose in `00-META/`, `03-DESIGN/`, `AGENTS.md`, `README.md`, and
research and issues dated from 2026-10-07 — code spans, quotations and link targets excepted — and on a
head word that heads two entries or is also retired. The words retired with no scope or with *(tools)*
are copied into the catalogue as `retired-words`, whose own repository check holds the descriptions of
the mesh's tools to them; a change here that retires such a word changes that copy in the same delivery.
Homonyms are checked by review.
@@ -0,0 +1,86 @@
---
status: graduated
initiated: 2026-10-10
touches:
- 02-DECISIONS/0019-how-this-repository-works.md
- 02-DECISIONS/0080-the-development-cycle-is-checked.md
- 00-META/process/03-issues.md
- 00-META/glossary.md
- the runtime's state verbs and the SDK's state interface
became:
- 03-DESIGN/01-to-be/53-the-issue-tracker.md
- 02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md
---
# 042 — The mesh keeps its own issues
## What
Whether the mesh should keep its issues in a module of its own, on the bus, instead of only as
numbered folders in this repository's `04-ISSUES/`, and what the smallest useful first version of
that module is. The operator's ask of 2026-10-10: work on a ticketing system on the bus, consolidate
every current issue into it, start small and extend the module along the way.
The effort answers six questions, in [01-findings.md](01-findings.md):
1. The smallest useful first slice: what the module holds, which tools it serves, which events it
emits, and what it explicitly leaves out.
2. Where the truth lives during and after the move: the folders here, the module, or a split
between them, and what each choice does to the public-repository rule, playbook 03 and the hq
checks.
3. How the 357 issue folders are imported and consolidated (duplicates, still-open ones, grouping).
4. How the controller's conditions, the router's asks and deliveries relate to issues.
5. How the operator sees issues: through the mesh's tools first, through a view onto the bus later.
6. The open questions for the operator, and the slices after the first.
The operator also named an existing project as inspiration: *hits*, a headless issue tracker built
as NATS micro services on JetStream, with an MCP server for agents. Its model is studied in the
findings, along with whether to reuse its code or build on its model.
## Why
Numbers collide. Playbook 03 tells an author to take the next free number across `main` and every
open pull request, and [issue 155](../../04-ISSUES/155-two-records-may-share-a-number-and-nothing-says-so/00-report.md)
records the second collision reaching `main` with every check passing. `cycle.py` now refuses two
records sharing a number, which catches a collision after it happens and does not prevent one. On
`main` at the time of writing, 31 commits mention a renumbering. On 2026-10-07 three decision numbers
collided in one evening. On 2026-10-10 an open pull request still carries a branch named for issue
349 and a title naming issue 350, because 349 was taken on `main` first. The operator's answer on
2026-10-07 was that numbering belongs to the mesh's own issue tracker.
Status is also hard to see. Of 357 issue folders on `main`, 73 are not closed (27 `open`, 45
`located`, 1 `diagnosing`). Finding them means reading frontmatter across the repository.
Nothing connects a condition the controller raises to the issue that would fix it: on 2026-10-10 a
condition was silenced with the words "hq issue to follow", and nothing records whether one followed.
## What it touches
- [ADR 0019](../../02-DECISIONS/0019-how-this-repository-works.md) (statuses live in frontmatter) and
[ADR 0080](../../02-DECISIONS/0080-the-development-cycle-is-checked.md) (the cycle is checked), with
playbook [03](../../00-META/process/03-issues.md) and `00-META/checks/cycle.py`.
- [ADR 0201](../../02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)
(a module keeps its state in key-value buckets it declares),
[ADR 0197](../../02-DECISIONS/0197-every-tool-announces-itself-on-the-bus-in-the-nats-services-protocol.md)
(tools announce themselves on the bus) and
[ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) (events are declared).
- The conditions of [ADR 0231](../../02-DECISIONS/0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md)
and the asks of [ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md).
- The records module, which indexes this repository for agents.
**Graduated 2026-10-10** into [ADR 0275](../../02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md)
and [to-be 53](../../03-DESIGN/01-to-be/53-the-issue-tracker.md). The operator delegated the five open questions; each
was decided as recommended. `touches:` names what the decision changes.
## Words
The glossary's word for one of these is **issue** (*The record*: "a report in `04-ISSUES/`"). This
effort keeps that word for the thing the module holds, and proposes **issue tracker** for the module
that holds them, with the module named `issues`. It does not use *ticket*, the operator's word in
the ask, or *item*, the word hits uses. If the glossary entry is widened (an issue is then held by
the issue tracker, and its narrative may live in `04-ISSUES/`), *ticket* would go on its *Not:* line.
That change belongs to the decision this effort graduates into, not to the effort itself.
## Open questions for the operator
Listed in [01-findings.md § 7](01-findings.md#7-open-questions-for-the-operator), each with a
recommendation.
@@ -0,0 +1,259 @@
# 01 — Findings
Measured on 2026-10-10 against `main` of this repository, the open pull requests on the forge, the
controller's open conditions, the module SDK's source, and a clone of the hits repository at its
release v0.7.0. Each section ends with what this effort recommends; section 7 lists what needs the
operator.
## 0. What exists today, measured
| Fact | Measured |
|---|---|
| Issue folders on `main` | 357, numbered up to 362; numbers 283 and 284 were never used, and 338, 341 and 350 sit on open pull requests (363 too) |
| Status in frontmatter | 283 `resolved`, 1 `wontfix`, 45 `located`, 27 `open`, 1 `diagnosing` |
| Frontmatter keys | `status`, `opened`, `located-in` on all 357; `fixed-by` 356; `amended-design` 344; `replay` 22; `replay-none` 44 |
| Size | 1.9 MiB of markdown in all; median folder 4.6 KiB, largest 25 KiB; 244 folders hold only a report, 111 also a diagnosis |
| Links between issues | 168 folders link another issue's folder (265 links); 193 name another issue in prose |
| `located-in` | free text: the controller is written `mesh-controller`, `cmd/mesh-controller`, `mesh-control` and as file paths; 17 issues name nothing |
| Renumberings | 31 commits on `main` mention one; one open pull request still carries the old number in its branch name |
| The forge's own issues | none in this repository or the controller's; [issue 178](../../04-ISSUES/178-a-routed-name-resolves-to-a-provider-merely-told-it/00-report.md) was filed there once and moved here, because "its numbers collide with hq's pull request numbers in conversation" |
| Conditions | 10 open; 5 of kind `healer-wanted` (a hand repair repeated, "an automatic repair is wanted"); one silenced with "hq issue to follow" |
Two things follow. The collisions come from allocating a number by reading a repository that many
branches write at once; nothing short of one allocator removes them. And the frontmatter is a small,
regular data set (six keys, 1.9 MiB) that fits easily in the bus's key-value store under the limits
[ADR 0201](../../02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)
§6 sets (256 KiB a value, 64 MiB a bucket).
## 1. The first slice
**A Go module `issues`, one instance on the control-node, serving one seat held once for the mesh.**
Its state is in buckets it declares (ADR 0201), its tools announce themselves on the bus
([ADR 0197](../../02-DECISIONS/0197-every-tool-announces-itself-on-the-bus-in-the-nats-services-protocol.md)),
and every change is an event it declares under `emits`
([ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md)). The module that delivers pull
requests already has this shape: Go, one mesh seat, two state buckets, declared events.
**What one issue holds in v1**, the frontmatter of today plus what the folders carry in prose:
- `number` (the key), `title`, `status` (`open`, `diagnosing`, `located`, `resolved`, `wontfix`),
`opened`, `closed`;
- `located-in`, from a registered list of repositories and modules rather than free text;
- `fixed-by` (pull requests or commits), `amended-design`, `replay` or `replay-none`;
- `notes`, appended and never edited: who wrote, when, and the text;
- `links`, typed: to another issue (`duplicates`, `relates-to`), to a pull request, a decision
record, a design, a condition key or a delivery;
- `record`: the path of the issue's folder in this repository, when it has one (section 2).
**Storage.** One bucket `issues` with one key per issue, keeping a short history per key, and a
`counter` key for the next number. The events (`opened`, `changed`, `noted`, `linked`) carry what
changed and who changed it, so they are the audit trail, and a view can watch the bucket. Section 6
compares this with a stream as the source of record.
**Tools** (verbs of the seat, found with `mesh_search`):
| Tool | Does |
|---|---|
| `open` | takes the next number atomically and writes the issue; refuses a title or report that is empty |
| `get` | one issue, whole |
| `list` | by status, by `located-in`, by a phrase in title or notes; compact rows, newest first |
| `move` | changes status, enforcing today's cycle rules at the write: `located` needs `located-in`; `resolved` needs `fixed-by`; a core issue needs `replay` or `replay-none`; `wontfix` needs a reason |
| `note` | appends to the trail |
| `link` / `unlink` | adds or removes a typed link |
| `import` | reads this repository's issue folders and writes them as issues with their own numbers (section 3); idempotent, reporting what it would change before it does |
**The caller is the bus credential, not an argument.** The module records who acted from the
identity the runtime gives it with each call, so an agent cannot write as someone else.
**Atomic numbering needs compare-and-set.** The SDK's state interface (v0.1.9) offers `get`, `put`,
`delete`, `keys` and `watch`, and no write that is refused when the key changed since it was read.
Whether the runtime's `mesh/state.put` takes an expected revision was not checked; the tool runner's
code was not read for this effort. Two ways out: add `create` and an `update` that takes the revision
read to the state interface (small, and every module with a counter gains it), or run exactly one
instance and number inside it. **Recommended: the first.** One instance is also right for v1, but the
counter should not depend on it.
> **Correction — 2026-10-10.** The finding above is wrong about the SDK. Version v0.1.9, the one read, had no
> compare-and-set, but the SDK's Go state interface already had it from go/v0.1.10: `Create` (writes a key only if it
> does not exist) and `Update` (writes only at the revision the caller read), with the runtime's matching state
> verbs, both from [ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md).
> What the tracker's counter needed and slice 1 added is `Increment`, in go/v0.1.12, which reads a counter and writes
> the next number with `Create` or `Update` at the revision it read, retrying a bounded number of times. The
> recommendation stands; the work it named was smaller than stated. ADR 0275 carries the same correction.
**Explicitly left out of v1:** decision and research numbers (slice 2), any change to playbook 03 or
the hq checks (slice 2), duplicate finding beyond links (slice 3), opening an issue from a condition
(slice 4), a web view (slice 5), full-text or semantic search, priorities, claims, notifications, and
the forge's own issues.
## 2. Where the truth lives
| | (a) Split: the tracker holds the issue, hq holds the narrative | (b) Everything moves | (c) The tracker mirrors hq |
|---|---|---|---|
| Number | tracker | tracker | hq, so collisions stay |
| Status, `located-in`, `fixed-by`, links | tracker | tracker | hq frontmatter, copied |
| Report and diagnosis | hq folder for a design or governance question; notes in the tracker for the rest | tracker notes | hq |
| Public-repository rule | holds: hq keeps only scrubbed narrative, the tracker is private and may name nodes | the public record of design questions is lost | holds |
| Playbook 03 | changes: open in the tracker first, write a folder only for a design or governance question | replaced | unchanged |
| `cycle.py` | its issue rules move into the tracker's `move`, refused at the write instead of at merge | same | unchanged, and a second copy to keep in step |
| Records module | still finds the narrative; the tracker's `list` and `get` serve status | finds no issues | unchanged |
Option (c) keeps the collisions and adds a second copy that can drift, so it answers neither of the
operator's problems. Option (b) loses the public record that hq exists to be
([ADR 0019](../../02-DECISIONS/0019-how-this-repository-works.md)), and with it the
symptom-to-component memory that playbook 03 says is never deleted.
**Recommended: (a).** Every issue gets a number from the tracker, including ones that never need a
document here (a node's quirk, an operational follow-up, which playbook 03's table already sends
elsewhere). A design or governance issue also gets a folder `04-ISSUES/NNN-slug/` with the tracker's
number, whose frontmatter shrinks to a pointer (`issue: NNN`) and whose prose stays reviewed in pull
requests. Status lives in one place, the tracker. This reverses ADR 0019's "statuses live only in
frontmatter" for issues and moves part of ADR 0080's check, so it needs a decision record that
supersedes those parts, written when this effort graduates.
**Numbering continues.** The tracker's counter starts above the highest number on `main` and on open
branches at the moment of the switch (today 363, so the first new issue is 364). Numbers 283 and 284
stay unused, and a number never moves again.
**During the move**, hq frontmatter stays authoritative until the import has run and been checked
against it, and one pull request then removes the status keys from every report and changes playbook
03 and `cycle.py` together. There is no period in which both are written.
## 3. Importing and consolidating the 357 issues
**Import.** `import` reads the folders through the records module (`records_list`, `records_read`),
over the bus, rather than from a checkout. Each folder becomes an issue with its own number, its
frontmatter as fields, its title from the heading, the commit it was read at, and its path as
`record`. Folder-to-folder links (265) become `relates-to` links. The four open pull requests'
issues (338, 341, 350, 363) are reserved as numbers and imported when they merge.
**Consolidating** means four concrete passes, each producing a list for the operator or an agent to
confirm, never applying itself:
1. **Close what is fixed.** A `located` or `open` issue whose `located-in` repository has a merged
pull request naming its number is a candidate for `resolved`. hits's auditor (section 6) checks
exactly this against git and is the model.
2. **Merge duplicates.** Candidates are pairs that link each other and share a `located-in`, and
pairs whose titles share most words. A confirmed duplicate is closed `wontfix` with a
`duplicates` link and the reason, keeping its number and folder.
3. **Normalise `located-in`** onto the registered list (`mesh-controller`, `mesh-catalog`, the
node-engine's repository, a module's name), keeping the file paths as a note.
4. **Group the 73 not closed** by domain (the glossary's domains, ADR 0244) and by `located-in`, so
the operator sees, for example, how many open issues the delivery domain carries.
## 4. Conditions, asks and deliveries
A condition is one open fact, raised and cleared only by observation (ADR 0231); an issue is a
question someone must answer. They are different things, and a condition must not be closed by
closing an issue. But a condition can say that a fix is owed: five of today's ten are `healer-wanted`,
and one silence says "hq issue to follow".
Recommended for a later slice (slice 4): an issue links a condition by its key; the controller shows
the link on the condition; and for `healer-wanted` the controller opens one issue per condition key
when it first raises it, linked both ways, instead of an agent remembering to. A silence whose reason
names an owed issue should carry its number. Asks
([ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md))
stay with the router: the tracker asks nothing in v1. Deliveries relate through pull requests: when
the forge says a pull request merged, the tracker notes it on every issue the pull request names, and
offers `resolved` (slice 4). Nothing in v1 depends on these.
## 5. How the operator sees issues
First through the mesh's tools: `list` returns rows short enough to read in a chat, `get` one issue
whole, and every agent session reaches them through the mesh MCP server. The responder (`@mesh`,
ADR 0234) answers from read verbs, so "what is open in delivery" works on a channel once the seat
exists. Later a view onto the bus: the `issues` bucket watched over the bus (a browser reaches it through a WebSocket listener, which this effort did not check for),
read-only, which the operator named as a future direction for every module's state. Nothing in v1
blocks it, because the state is already a bucket and every change an event.
## 6. hits: what it is, and what to take from it
**What it is.** A headless issue tracker in Go (module `github.com/impire-io/hits`), with no web
interface and no HTTP interface: NATS micro services on JetStream, a command line for people and a
stdio MCP server for agents. One author, 81 commits between 2026-09-02 and 2026-09-14, release
v0.7.0, about 9,700 lines of Go outside tests and 21 test files, tested against an embedded NATS
server. Its model descends from a method like this repository's: a sibling `hits-hq` with numbered
playbooks and decisions.
**Data model.** An *item* has a type (`bug`, `task`, `improvement`), a status (`open`, `diagnosing`,
`located`, `blocked`, `resolved`, `wontfix`), a priority, `located-in` (from a registered list of
*projects*), `fixed-by` (pull request, commit or action), `amended-design`, append-only notes, typed
links (`duplicates`, `relates-to`), a claim, a block, and a tombstone for a filing mistake. Its
statuses and fields are this repository's frontmatter almost key for key. Projects, *initiatives*
(groups of projects that also scope the numbering) and *releases* are registered lists that are
never reused once retired.
**How it stores.** The source of record is a stream `hits-ops` with subjects
`hits.ops.item.<id>` (and `project`, `initiative`, `release`). Every write appends an operation,
published with the per-subject expected last sequence, so two writers to one item cannot both win.
A bucket `hits-state` holds the folded snapshot of each item, the registered lists and one counter
per initiative. The counter is advanced by compare-and-set and an item is numbered only after it is
validated, so a refused create spends no number. On start it rebuilds the bucket from the stream.
**Services and subjects.** One micro service `hits` with 23 endpoints under `hits.api.*` (`create`,
`get`, `edit`, `transition`, `claim`, `release`, `block`, `unblock`, `link`, `unlink`, `note`,
`tombstone`, and register, list and retire for projects, initiatives and releases); three index
services, each a projection of the stream: full text (`hits.search.query`, a local index on disk),
a graph (`hits.graph.neighbors`, `hits.graph.walk`) and an optional semantic search that needs an
outside embedding provider.
**MCP tools.** 27, one per endpoint: `create_item`, `get_item`, `edit_item`, `transition_item`,
`claim_item`, `release_item`, `block_item`, `unblock_item`, `link_items`, `unlink_items`,
`note_item`, `tombstone_item`, the project, initiative and release tools, and `search_items`,
`semantic_search`, `graph_neighbors`, `graph_walk`. One server serves one agent under one actor.
Its command line also has an auditor that checks every item's `fixed-by` against git and finds
merged pull requests whose items are still open.
| | Adopt as it is | Adapt to the mesh |
|---|---|---|
| Statuses, fields, append-only notes, typed links, tombstone | yes: they are already ours | — |
| Stream of operations as the record, bucket as its projection | the pattern | the stream and bucket declared in the manifest and created by the controller (ADR 0201 §3), not created by the service on start |
| Expected-sequence writes; compare-and-set counter; number only after validation | yes | needs compare-and-set in the state interface (section 1) |
| The auditor | the idea, as a tool | reads the forge through the gitea module, not a local clone |
| Subjects `hits.api.*`, its own MCP server | no | the seat's verbs, served by the tool runner and found with `mesh_search` |
| Actor named by the caller | no | the caller is the bus credential the mesh issued |
| Contexts, OAuth device flow, semantic search | no | the mesh issues credentials; no outside provider |
| *item*, *project*, *initiative*, *release* | no | *issue*; a repository or module; no initiatives in v1; *release* is already a homonym here (glossary) |
**Reuse its code, or build on its model?** Its licence is the Sustainable Use License 1.0: use and
modification are allowed for internal business, non-commercial or personal use, and distribution only
free of charge and for non-commercial purposes. It is not an open-source licence. Running it on a
personal mesh is allowed; vendoring it into the catalogue binds the catalogue to that licence for as
long as the code is there. Its services create their own stream and bucket on start, which ADR 0201
§3 forbids a module to do; its subjects carry no prefix and assume one tracker per bus account; its
words clash with the glossary. Changing those means a fork of a two-week-old, single-author project.
v1 here is the item store without the indexes: roughly the size of the module that delivers pull
requests.
**Recommended: build our own module on hits's model,** and say so in the module's README. Do not
depend on or vendor its code. Revisit only if its licence changes, or if its author agrees to a
licence for this use.
## 7. Open questions for the operator
1. **Should the tracker hold every issue's number and status, while hq keeps a folder only for design
and governance issues, pointing at the tracker by number (option (a) in section 2)?**
Recommended: yes. It ends the collisions, keeps the public record, and moves the cycle rules into
the write, where they refuse instead of report.
2. **Should we build our own `issues` module on hits's model rather than run or vendor hits?**
Recommended: build our own. hits's licence limits distribution, its services create their own bus
resources (against ADR 0201), and its words clash with ours. Its model, its compare-and-set
numbering and its auditor are worth copying.
3. **Should the tracker also hand out decision and research numbers?** Recommended: yes, in slice 2.
Decision numbers collided three times on 2026-10-07; the same counter mechanism serves any
sequence, and the tracker then reserves the number when a branch starts.
4. **Should the word stay *issue*, with the module named `issues` and *ticket* retired?**
Recommended: yes. *Issue* is the glossary's word for this, and one word per thing (ADR 0244).
5. **Should the controller open an issue by itself for a `healer-wanted` condition, or only link
one an agent or the operator opened?** Recommended: link only, until slice 4 has run for a week;
then open by itself for `healer-wanted`, one issue per condition key.
## 8. Slices after the first
| Slice | Delivers | Done when the operator sees |
|---|---|---|
| 1 | The `issues` module: open, get, list, move, note, link, import; compare-and-set in the state interface | the next issue number comes from the tracker and the 73 not closed are listed by one call |
| 2 | Decision and research numbers from the tracker; playbook 03 and `cycle.py` changed; status keys removed from every report | two agents opening at once get two numbers, and no hq pull request is renumbered |
| 3 | Consolidation: the four passes of section 3, and an audit tool for `fixed-by` against the forge | the not-closed list shrinks by what was already fixed or duplicated, each with its reason |
| 4 | Conditions and pull requests linked; `healer-wanted` opens an issue | a condition shows its issue, and a merge notes the issues it names |
| 5 | A read-only view onto the `issues` bucket | the operator reads issues in a browser without an agent |
@@ -0,0 +1,112 @@
---
status: graduated
initiated: 2026-10-10
touches:
- 02-DECISIONS/0019-how-this-repository-works.md
- 02-DECISIONS/0080-the-development-cycle-is-checked.md
- 02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md
- 02-DECISIONS/0248-the-decision-index-is-generated-when-it-is-read-and-never-stored.md
- 02-DECISIONS/0274-an-agent-proposes-a-shared-instruction-section-and-every-node-takes-it-only-on-the-operators-warrant-which-it-proves-itself.md
- 02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md
- 03-DESIGN/01-to-be/53-the-issue-tracker.md
- 00-META/process/
- 00-META/checks/
- 00-META/glossary.md
- the records module's tools
became:
- 02-DECISIONS/0290-a-decision-record-lives-whole-in-the-knowledge-base-made-of-clauses.md
- 02-DECISIONS/0291-an-issue-lives-whole-in-the-issue-tracker.md
- 03-DESIGN/01-to-be/55-the-knowledge-base.md
---
# 045 — A knowledge base the mesh writes: decisions, guidelines, policies
## What
Whether the mesh should **write** its own knowledge (decision records, guidelines, policies, designs and
playbooks) through a module's verbs, instead of only **reading** hand-written markdown, and whether it
should hold light content beside them: meeting notes, architectural drawings and blog posts.
The operator's purpose, given on 2026-10-11: the record becomes **shared knowledge that agents reach
through tools**, so that an agent gets what was decided as an answer instead of reading markdown files,
and a session no longer has to start in this repository's folder to know it. A session still starts here
for other work, such as writing a record. The operator later put the light content last, and decided that
an issue's content belongs in the issue tracker rather than in this repository.
Today the record is read-only. The `records` module reads this repository and serves it to sessions
([ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)).
Everything in it is written by hand as markdown and reaches `main` through a pull request; the hq checks
in `00-META/checks/` judge it only after it is written. The issue tracker has already taken issues out
of this path: it numbers and tracks them in a module of its own, and hq keeps only the narrative
([ADR 0275](../../02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md)).
## Why
Numbers collide, and the checks find a collision only after it happens. On 2026-10-10, research number
043 was taken by one open pull request while a second session held an unpushed branch claiming the same
number for a different effort. On 2026-10-07 three decision numbers collided in one evening. A
record written by a verb could take its number from one counter, the way the issue tracker does.
Reading is costly too. Answering one question about what holds today means reading several records
whole, and the generated decision index is larger than one tool answer may be.
Some knowledge also has no home. This repository is public, so it may not name nodes or describe
operations; the mesh's knowledge about its own installation lives in agents' private notes, in no
shared and searchable place.
## The two options to weigh
- **A. Git stays the store.** The `records` module gains write verbs (propose, amend as a progressive
insight, supersede, retire), each of which makes a branch and a pull request through the forge. Numbers
come from the issue tracker's counters, as a further sequence beside `issue`
([to-be 53](../../03-DESIGN/01-to-be/53-the-issue-tracker.md) §4).
- **B. A store is the truth, and hq is its rendered public export**, as the issue tracker already is for
issues (ADR 0275).
## The questions
1. **Types and lifecycles.** Which kinds of knowledge exist: decision, guideline, policy, design,
playbook, and any others? What lifecycle does each have (proposed, accepted, superseded, retired;
to-be and as-is for a design), and which transitions does each kind allow?
2. **Numbering from one counter.** One sequence per kind, or one for all? How is a number reserved for
a draft that is later abandoned, given that a number is never reused? How are today's collisions
(043 is the latest example) resolved on import?
3. **Typed links.** Which links between records are needed: supersedes, amends, implements, cites,
decided-by, and others? Which of them does frontmatter express today (`became:`, `decisions:`,
`fixed-by:`), and which exist only as prose links that no check reads?
4. **Approval.** A decision or a policy changes how the mesh is run. Should accepting one require the
operator's warrant on a channel that proves who answered, as a shared instruction section already
does ([ADR 0274](../../02-DECISIONS/0274-an-agent-proposes-a-shared-instruction-section-and-every-node-takes-it-only-on-the-operators-warrant-which-it-proves-itself.md))?
Which kinds need it, and which may an agent write on its own?
5. **Public and private knowledge.** Node names and operational knowledge have no home today. Does the
knowledge base hold private records beside public ones, with a rule for which may be exported? How is
a record checked so that nothing private reaches the public repository?
6. **Checks as refusals at write.** `records.py`, `cycle.py`, `words.py` and `index.py` judge a pull
request after it is written. Which of their rules can become refusals of the write verb itself, and
which only make sense across the whole record?
7. **Import.** How are the decision records (274 on `main` on 2026-10-11, numbered up to 0289), the designs and the playbooks imported without
losing their history, their numbers or their links? Is the import repeatable, as the issue tracker's
is?
8. **What git gives today.** Review on a pull request, line comments, a history of every change with
its author, a diff, a revert, and the forge's required checks. What of that would a store have to
rebuild under option B, and what would option A lose by making every write a pull request?
## Words
The glossary has words for **decision record**, **design**, **playbook** and **the record** (the
`records` module), but none for **guideline** or **policy** in this sense, and *policy* is already used
for a module's **upgrade policy**. This effort uses "guideline" and "policy" as plain descriptions, and
"knowledge base" for the whole, until a decision settles the words through the glossary's own rules.
## What it touches
How this repository works (ADR 0019) and how the development cycle is checked (ADR 0080), the
`records` module (ADR 0153) and the generated decision index (ADR 0248), the issue tracker and its
counters (ADR 0275, to-be 53), approval on a proving channel (ADR 0274), the playbooks in
`00-META/process/`, the hq checks in `00-META/checks/`, and the glossary.
## Documents
- [01 — Findings](01-findings.md): what exists, measured on 2026-10-11; where the truth lives (the store
for each record's structure and rules, this repository for its reasoning); issues whole in the issue
tracker; the order of work; and what is still open.
@@ -0,0 +1,295 @@
# 01 — Findings
Measured on 2026-10-11 against `main` of this repository, the records module and the issue tracker as they
answer through the mesh MCP server, and the hq checks in `00-META/checks/`. Section 0 is what exists; section 1 is where the truth lives, which
decides the rest; sections 2 to 9 answer the overview's questions and the operator's additions; section 10 is
the order of work, and section 11 what is still open.
## The purpose, in the operator's words
The operator gave the purpose on 2026-10-11: the decisions and the rest of the record should become **shared
knowledge that agents reach through tools**, instead of reading markdown files, and a session should no longer
have to start in this repository's folder to know what was decided. A session still starts here for other work,
such as writing a record. Later that day the operator widened the scope to **light records**
(meeting notes, architectural drawings, blog posts) and then put them last; asked for a structured store of
the decisions and research efforts; and decided that an issue's content belongs in the issue tracker, not in
this repository.
That purpose moves the weight of the overview's questions. Where the truth is stored (option A or B) matters
less than **what an answer looks like**: an agent needs the rule that holds now, with the records it rests on,
in one call and a few kilobytes.
An acceptance sentence for the whole effort: *a session started in any folder on any node asks "what holds
about the merge window, and why" and gets, in one call of under 4 KiB, the rules in force, the records they
come from, what changed them since, and the commit it was read at.*
## 0. What exists today, measured
| Fact | Measured |
|---|---|
| Records on `main` | 274 decision records, 44 research efforts (119 files), 55 to-be and 16 as-is designs, 8 playbooks, 361 issue narratives (479 files): 951 files, and the glossary |
| Size | 6.9 MB of markdown, about 1.7 million tokens at four bytes a token; decision records alone 2.6 MB (about 650,000 tokens) |
| One question, today | "what holds about the merge window": `records_search` gives 26 places in 11 documents; reading the five records and the design they point to is 85 KB, about 21,000 tokens |
| The decision index | `records_decisions` answers 77 KB in one piece; the agent's tool-answer limit refused it inline, and it had to be read from a file in parts |
| What `records_search` does | a literal, case-insensitive phrase match giving document, line and heading; no ranking, no notion of which record is still in force |
| Frontmatter of a decision | `topic`, `status`, `date`, `deciders` on all 274; `extends` on 229 (182 with the folder prefix, 47 without); `supersedes-in-part` on 15 (21 links), `supersedes` on 8, `superseded-by` on 9, `rests-on` on 2, `amended` on 1 |
| Status of a decision | 265 `accepted`, 9 `superseded`; no record is marked as changed in part, though 21 links say so |
| Supersession said only in prose | 6 records say they supersede, amend or replace another and carry no field for it; 7 records carry a "superseded" marker in the body, of which only 1 has the field or the status |
| Progressive insights | 39 records carry the dated marker, 55 markers in all; 2 more use an older form |
| Change after writing | 189 of 271 traceable decision records were changed after the commit that created them; 34 paths were changed five times or more |
| Pace | commits touching decision records: 87 in the last ten days of August, 160 in September, 274 in the first eleven days of October |
| Number collisions | 16 decision numbers were taken twice by sessions working at once (2026-09-25 to 10-10), each repaired by a renumbering commit; 15 numbers on `main` are gaps; no research number collided on `main` (the 043 collision of the overview never reached it) |
| A decision's rules | 263 records have a `Decision` section; only 37 of them write it as numbered rules with a bold lead (198 rules in all); the rest write prose |
| Drawings | no file but markdown in the repository; no diagram source; 22 designs carry diagrams drawn in text characters |
| Knowledge with no home | the repository is public, and its README says what only makes sense to someone who knows this installation "is either a note in the wrong place or a disclosure"; one agent's private notes hold 89 files for one repository on one node, and issue 367 (a session's memory stays on the node where it was learned) is open |
| The hq checks | 33 rules: 25 judge one record, 4 the whole record, 4 the prose (section 7) |
Three things follow.
1. **The cost of an answer is the problem, not where the files are.** 85 KB for one question, and an index
too large for one tool answer, mean a session reads in parts or reads less than it should. Moving the
markdown into a store without changing the shape of the answer would keep that cost.
2. **The data that makes an answer cheap is missing or loose.** What a record decided is prose in 226 of 263
records. Which records changed which is half in fields, half in prose: 6 records say it only in prose, and
6 more carry a body marker that no field or status agrees with.
Status says `accepted` for records that 21 later links change in part.
3. **The records change after they are written, and that is reviewed.** 189 of 271 decision records were
edited after creation, every edit through a pull request with a human approval. Whatever is built must keep
that review for what governs the mesh.
## 1. Where the truth lives: what a clean start would choose
The operator asked on 2026-10-11 which option this effort would choose with no prior setup. The answer
decides the rest, so it comes first.
**The unit the mesh reasons about is a rule, not a file.** A decision is a handful of rules. A later decision
changes one of them: ADR 0282 replaces one rule of ADR 0276 and leaves the others standing. Git and markdown
know files only, so that change can be written only as prose. The earlier record keeps `accepted`, and every
reader must read both records to learn what holds. Section 0's loose links all have this one cause: 21 partial
supersessions with no reverse, 6 said only in prose, 6 markers no field agrees with.
**A store can hold what a repository cannot.** In a store, each rule is an entry with its own status and links
("in force", "changed by rule 2 of 0282"). A write that leaves the record inconsistent is refused when it is
made, not found by a check afterwards. Numbers come from one counter. "What holds now" is one query.
**What git does best is review of prose.** The reasoning behind a decision (its context, the options weighed,
its consequences) is read and commented on by people. Agents rarely ask for it. They ask what holds.
So a clean start divides **by layer, not by kind**:
- **the store is the truth for the structure** of every governing record: its number, title, kind, topic,
status, its rules one by one, and its typed links, at the level of the rule where a link is about a rule;
- **this repository keeps the reasoning** as markdown under the store's number, reviewed in pull requests.
That is the division the issue tracker already made for issues
([ADR 0275](../../02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md)):
the tracker holds number, status and links, and hq held the narrative. Section 9 takes issues one step further,
on the operator's word: their narrative leaves this repository too.
Option A (git stays the truth, and a module's write verbs open pull requests, with numbers from a counter) was weighed and is not chosen. It keeps review, but it
keeps the rule-level links in prose and the refusals after the fact, which is the cause of section 0's mess.
Option B in full (the reasoning moves into the store too) is not chosen either: it would rebuild review, line
comments, history and diff for prose, which git already gives.
## 2. Kinds and their lifecycles
| Kind | Lifecycle | Notes |
|---|---|---|
| decision | proposed → accepted → superseded; a rule can be changed or superseded on its own; a dated progressive insight corrects a fact | 274 records |
| research | active → graduated or abandoned | 44 efforts |
| design | proposed → designed → in-progress → implemented, in one of two layers (to-be, as-is) | 71 documents; a later slice (section 10) |
| playbook | in force, or retired | 8, with no frontmatter today; a later slice |
| glossary entry | in force, or retired with its replacement | one file today; a later slice |
The first slice holds decisions and research, the two the operator named.
A **guideline** or a **policy**, which the overview named, has no record of its own today. What the operator
calls a policy is written as a decision; what an agent should do is a shared instruction section (ADR 0274) or
a personal rule ([ADR 0273](../../02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md)).
No new kind is proposed for either.
**Light records** (meeting notes, drawings, blog posts) were added to the scope by the operator on 2026-10-11
and put last by the operator the same day. They govern nothing, need no review to exist, and are private by
default; a drawing's file belongs in an object-store bucket
([ADR 0288](../../02-DECISIONS/0288-the-object-store-stays-on-the-maintained-fork-and-garage-is-chosen-only-after-the-consumers-s3-features-are-counted.md))
with a text description an agent reads. They are a later slice (section 10) and shape nothing in the first.
## 3. Numbering from one counter
The 16 concurrent collisions all come from reading the next number off a repository that many branches write
at once. The issue tracker hands out numbers by compare-and-set on a counter key
([to-be 53](../../03-DESIGN/01-to-be/53-the-issue-tracker.md) §4); the knowledge base asks it for every number.
- **One sequence per kind**: decision, research, and later design. Numbers keep their meaning in conversation
("ADR 0276").
- **A reserved number is spent.** A proposal that is abandoned keeps its number with status `withdrawn`, who
reserved it and when. Gaps already exist (15 on `main`) and harm nothing; a reused number would.
- **The counter starts above the highest number on `main` and on every open branch**, as the tracker's did.
- **The counters stay where ADR 0275 put them.** Its rule 8 decided that the issue tracker hands out decision
and research numbers too, in its second slice; to-be 53 §4 so far names only the issue counter. The knowledge
base's `reserve` asks the tracker for the number, so one counter holds every sequence and no accepted record
is superseded for it.
## 4. Rules and typed links
**A rule** is one short statement a decision makes, numbered within its record ("0276 rule 1"), with its own
status: `in force`, `changed by` a rule of a later record, or `superseded`. 37 of 263 decision sections already
write numbered rules with a bold lead (198 rules); the other 226 write prose, from which the import drafts the
rules (section 7).
The links the first slice holds, each stored in both directions:
| Link | From → to | Today |
|---|---|---|
| supersedes | record → record | `supersedes` / `superseded-by`, 9 of 9 mirrored |
| changes | rule or record → rule | `supersedes-in-part` on 15 records, one direction; 6 more only in prose |
| extends | record → record | `extends` on 229, two spellings |
| became | research → decision or design | `became` |
| decided by | design → decision | `decisions` |
| corrects | progressive insight → record | the dated marker, 55 in 39 records |
| cites | any → any | prose links: 1,785 decision to decision, 469 decision to issue |
"What holds about X" is then: the rules matching X whose status is `in force`, each with its record and the
changes that brought it to its present wording.
## 5. Approval
A decision governs how the mesh is built and run, so it needs a human approval before it holds.
- **Proposing** reserves a number and writes the structure with status `proposed`. Any agent may propose.
- **Accepting** happens when the pull request carrying the record's reasoning merges with its human approval
and both required checks. The knowledge base sees the merge on the bus and moves the record and its rules to
`accepted` and `in force`, and every rule it changes to `changed by`. The merge is the warrant; a second
approval beside it would split the evidence.
- **A progressive insight** is a correction of fact and goes the same way: a pull request on the reasoning,
and the structure recorded on merge.
- A **structural write without a pull request** (a link added, a rule's wording fixed) is refused for an
accepted record. Changing what an accepted record says is a new decision, as it is today.
## 6. Public and private
This repository is public and stays so. The reasoning of a decision or research effort stays here, public.
The structure in the store is derived from public records and may be exported here as generated pages, like
the decision index of [ADR 0248](../../02-DECISIONS/0248-the-decision-index-is-generated-when-it-is-read-and-never-stored.md),
generated when read and never stored.
What changes is the issues' narrative (section 9): it moves into the tracker, which is the mesh's own and not
public. The public repository loses those narratives. ADR 0275 kept them public on purpose, citing *the mesh
remembers*; the tracker remembers them as well, but only for the mesh. Whether a resolved issue's narrative is
ever exported publicly is a question for the decision that supersedes that part of ADR 0275.
Knowledge about this installation (one agent's 89 private notes, issue 367) stays out of scope. Agents' lessons
and rules belong to the agent module's register (ADR 0273).
## 7. Checks as refusals at write, and the import
Of the 33 rules the hq checks enforce, **25 judge one record** and become refusals of the knowledge base's
write verbs: a number taken twice, a supersession not mirrored, a cited record not accepted, a record without a
topic, a research effort graduated without `became`. Two need a look beyond the record: a write that
supersedes must check who cites what it supersedes. **4 judge the whole record** and **4 judge the prose**;
those stay checks on this repository, run on every pull request. As frontmatter leaves this repository
(section 8), the 25 leave `records.py` and `cycle.py` with it, and a check is added that a record's reasoning
names a number the knowledge base holds.
**The import** reads this repository at a commit through the records module and writes each decision and
research effort at its own number: status, topic, date, deciders, links, and its rules. Like the tracker's, it
creates what is absent, never overwrites, never advances a counter, and a second run changes nothing and says
so. Two things a reading cannot recover alone:
- **Loose links.** The 6 supersessions said only in prose, the 6 markers no field agrees with, the 47 bare
`extends` targets and the 21 one-way partial supersessions are repaired in this repository first, by one
pull request, so the import reads clean input.
- **The rules of 226 decisions written as prose.** The import drafts them through the agent's judgement
([ADR 0278](../../02-DECISIONS/0278-the-issue-tracker-checks-every-new-issue-for-a-duplicate-through-a-judgement-the-agent-makes-without-tools.md))
and stores them as `drafted`, not `in force`. A topic's drafts are confirmed by the operator or a reviewer in
one pass. An answer marks a drafted rule as drafted, and never presents it as the record's word.
## 8. What leaves this repository, and what stays
| | Today, in this repository | After |
|---|---|---|
| a decision's number, status, topic, links | frontmatter | the knowledge base; the file keeps the number in its name and heading |
| a decision's rules | prose in the `Decision` section | the knowledge base; the section stays as prose for the reasoning |
| a decision's context, options, consequences | markdown | unchanged |
| a research effort's status and `became` | frontmatter | the knowledge base |
| a research effort's documents | markdown | unchanged |
| the decision index | generated from frontmatter when read | generated from the knowledge base when read |
| an issue's report, diagnosis, resolution | `04-ISSUES/NNN-slug/` for 361 issues | the issue tracker (section 9) |
Frontmatter leaves a file only after the import has proven it holds the same values, one kind at a time, as
ADR 0275 did for issues' status.
## 9. Issues live in the issue tracker, whole
The operator decided on 2026-10-11 that an issue's content belongs in the issue tracker, not in this
repository. Measured on `main`: 361 issue folders, 1.9 MB; a folder's median is 4.4 KB and the largest
25.9 KB, well under a tracker value's 256 KiB. Every folder has `00-report.md`; 107 have a diagnosis, 9 a
resolution, 1 a progress file. Since ADR 0275, new issues live in the tracker already; the highest issue folder
is 364.
What the tracker needs to hold it:
- **an issue's documents**: the report it already holds, plus a diagnosis and a resolution as named documents
of the issue, each with its author and date, revised in place with history (notes stay append-only);
- **an import** of the 361 folders, repeatable, keeping each issue's number and dates;
- **links** from records: 250 files in this repository mention a path into `04-ISSUES/` 765 times, counted
as every `04-ISSUES/NNN` path in the text (markdown links alone are fewer, about 660). Those mentions become
plain "issue N" references that the knowledge base and the tracker resolve, rewritten in one pull request
once the import is proven; the link check in `records.py` then refuses a new link into `04-ISSUES/`.
This supersedes part of ADR 0275 (hq keeps the narrative of a design or governance issue), so it needs its own
decision record.
## 10. Slices
1. **Clean the input** (this repository, one pull request): repair the loose links of section 7, so the import
reads clean records. In review as hq pull request 305, which declares every partial supersession in both
records and adds the `partial-supersession` check. Beside it, a shared instruction section for every
session: take a decision or research number above every one on `main` and on open branches until the
counter exists, and name a partial supersession in a field, never only in prose.
2. **The knowledge base, decisions and research**: a Go module serving one seat held once for the mesh, on
the control-node beside the issue tracker. The import, the rules and links, and the answers; numbers come from the issue tracker (section 3):
| Verb | Answers |
|---|---|
| `ask` | the records and rules that bear on a question, ranked: number, kind, status, title, and whether each rule still holds or what changed it |
| `get` | one record: its rules, its links both ways, what changed it since; its reasoning's text only when asked |
| `holds` | for a topic or a record, the rules in force now, each with the record it comes from |
| `reserve` | the next number of a kind, from the issue tracker's counter (section 3), spent whether or not the record lands |
| `propose` | a new record's structure, status `proposed` |
| `import` | this repository's decisions and research at a commit, repeatable |
Storage is a database in the store, provided to the module: the structure is small, but `ask` needs ranked
full-text search, which the store's server does and a bucket does not. Every answer is bounded in size.
3. **Frontmatter leaves this repository**, kind by kind, once the import is proven equal.
4. **Issues whole in the tracker** (section 9): documents, import, links rewritten, folders removed.
5. **Designs, playbooks and glossary entries** into the knowledge base the same way.
6. **Light records**: notes, posts and drawings.
How a session reaches it: the knowledge base's tools reach every session through the mesh MCP server, and one
shared instruction section, approved by ADR 0274's route, tells every session to ask the knowledge base what
holds before reading files. A session needs this repository's folder only to write a record's reasoning.
Proved by the acceptance sentence at the top: the merge window question, asked from a session outside this
repository, answered in one call under 4 KiB, naming ADR 0276's rules in force and the one ADR 0282 changed.
## 11. What needs the operator
Decided by the operator on 2026-10-11: the store is the truth for structure and this repository for reasoning
(section 1); light records come last; issues live whole in the tracker (section 9).
Still open:
1. **Drafted rules (section 7)**: the 226 prose decisions get their rules drafted by the agent's judgement and
confirmed a topic at a time. Who confirms: the operator, or an independent reviewing agent with the operator
seeing a summary?
2. **Issues' public narrative (section 6)**: once issues leave this repository, is a resolved design or
governance issue ever exported here publicly, or does it stay the mesh's alone?
## Words
The glossary has **decision record**, **design**, **playbook** and **the record** (the records module). This
effort proposes, for a decision to settle through the glossary's own rules: **knowledge base** for the module
and its seat; **rule** for one statement a decision makes, with its own status; **governing record** for a kind
reviewed before it holds; and **note**, **post** and **drawing** for the light kinds. *Policy* stays the word
for a module's upgrade policy, and *guideline* is not proposed, since section 2 finds no record of that kind.
@@ -0,0 +1,64 @@
---
topic: building it
status: accepted
date: 2026-04-03
deciders: jochen
reconstructed: true
---
# 11. Managed files are generated onto nodes and never edited there
> Reconstructed after the fact from the evidence cited below.
## Context
[ADR 0006](0006-the-substrate-and-the-control-plane.md) put every binding in the mesh
database. But the things that consume those bindings — environment files, service
definitions, daemon configuration, firewall rules — are files on a node's disk, because that
is what the software reading them requires.
So the same value exists twice: authoritatively in the database, and materialised in a file.
Any edit to the file is a change to a copy. Before this decision, environment values could be
pushed from a node back into the database, which made the direction ambiguous in both
directions at once.
## Considered options
1. **Bidirectional sync** — a node's edits flow back to the database. Rejected, and removed.
Two writers and no arbiter: whichever synced last wins, and neither is authority.
2. **Files are authoritative; the database is a cache of them.** Rejected — it inverts
ADR 0013 and returns to state that cannot be reconciled across nodes.
3. **Strictly one-directional: the database is written, files are generated.** Chosen.
## Decision
Every managed file is **derived**. A synchroniser regenerates it from the mesh database
whenever the underlying values change. The write path is the mesh tool that owns the value;
the file is an output.
This applies to generated environment files, service definitions, managed configuration, and
anything else a synchroniser lists as its own.
An edit to a managed file survives until the next synchronisation and is then overwritten,
without a warning, taking whatever it was fixing with it.
## Consequences
- **A file edited on a node is a bug with a delay on it.** This is now one of the mesh's core
values, and it is a consequence of this decision rather than a stance taken independently.
- To change a value you must know which tool owns it. That is a real cost, paid every time,
and the reason the mesh provides a way to ask whether a given file is managed.
- Debugging by editing a file no longer works, and fails in the most confusing way available:
it works, and then stops working later for no locally visible reason.
- Recovery is cheap. A node's entire managed surface can be regenerated from the database.
- Values resolve by precedence — database override, then existing file value, then generated,
then manifest default — which means an unset override does not clobber a generated
password. The subtlety is real and has caused its own confusion.
## References
- `Extract hal/env-sync module, remove syncEnvToDb`, 2026-04-03 — the commit that removed the
node-to-database direction.
- Knowledge base: `conventions/no-direct-file-mutation`, `provisioning` (resolution priority).
- Regeneration gaps: `troubleshooting/changed-manifest-default-not-rerendered`,
`troubleshooting/config-removed-from-manifest-not-pruned`.
@@ -0,0 +1,104 @@
---
topic: the mesh
status: superseded
superseded-by: 0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
date: 2026-09-26
deciders: jochen
reconstructed: false
supersedes: 02-DECISIONS/0125-the-bus-is-the-only-broker.md
---
# 127. AMQP is a provision, not the bus
## Context
[ADR 0125](0125-the-bus-is-the-only-broker.md) decided that the bus is the only broker, and went
one step further than it had grounds for: it also decided that the `amqp` **interface** — a module
requiring a message broker of its own — "is not carried forward" and "retires with the
compatibility broker rather than gaining a successor", with the two modules declaring it converted
to the bus in step 4.
The operator's correction: **AMQP is deprecated as the mesh's transport, not abolished as a
service.** The broker module keeps running and keeps answering `amqp` requirements. It is no
longer a core part of the mesh — *"it's just a module like mssql now."*
**What 0117 conflated** is two different reasons a module might ask for a broker, which look
identical in a manifest:
1. **To talk to other modules.** Wrong under one bus, and the thing 0117 was right to refuse: a
private broker used as inter-module transport is a second bus, with every guarantee crossing a
seam and no scoping the mesh can see.
2. **Because it genuinely needs an AMQP broker**, the way something needs a database — a queue for
its own internals, or interop with software that speaks AMQP and nothing else. That is a
backing service, and the mesh has a word for backing services already.
0117 saw the first and legislated against both. The second is ordinary, and forbidding it would
make the mesh unable to run a large class of perfectly normal software while claiming that as
architecture.
## Considered Options
1. **Keep 0117 as written** — retire the interface, convert the two modules. Rejected by the
operator, and wrongly reasoned besides: it treats "needs an AMQP broker" as always a mistake.
2. **Keep the broker as the predecessor's compatibility module**, as ADR 0106 framed it, with a
retirement condition. Rejected: it is not single-purpose and its clients are not only the
predecessor's, so the retirement condition describes a day that will not come.
3. **The broker is an ordinary provider module of an ordinary provision.** Adopted.
## Decision
**The mesh's bus is NATS and only NATS.** Everything 0117 decided about *the bus* stands: one bus,
a module's messaging is subjects on it scoped by what it declares, no module is handed a bus of
its own, and the `mesh-broker` seat is the NATS server's.
**`amqp` remains a provision a module may require**, answered by the broker module the way
`postgres-database` is answered by the store module or a database is answered by mssql. It is not
deprecated as an interface; the software behind it is simply no longer the mesh's nervous system.
**The broker module stops being foundation.** It claims no seat — `mesh-broker` is the NATS
server's — it is not raised at genesis, nothing in the mesh requires it, and a mesh that never
installs it is a complete mesh. It is installed when something wants it, like any other provider.
**The rule that survives, stated so it can be applied:** *inter-module communication goes over the
bus.* A module may hold a broker, a database or a cache as a backing service; it may not use one
as a channel to another module. The line is not which software is involved, it is whether a second
module is on the other end.
**Neither `amqp-ping` nor `amqp-email-forwarder` needs converting.** 0117 put that work in step 4;
it is removed. They require a backing service and a provider answers.
## Consequences
- **The "compatibility broker" framing is wrong and goes.** There is no `lavinmq-compat`, no
single purpose and no retirement condition. Design 25 §5 is corrected.
- **[ADR 0106](0106-the-bus-is-nats.md)'s progressive insight was itself wrong** and is corrected
by a second one there. It said 0117 would make 0106's "one purpose — the predecessor's clients"
sentence true by moving the mesh's modules off. Nothing moves off; the sentence is simply not
what the broker is.
- **The seat change stands**, for a better reason than 0117 gave: not because a broker cannot be
provisioned, but because *this* broker is not the mesh's bus. The broker module drops its
`mesh-broker` claim and the `nats` module takes it.
- **Step 4 loses two conversions**; step 1 and the WBS are otherwise unaffected.
- **What got harder:** the rule is now a judgement rather than a prohibition. "Is this a backing
service or a channel to another module?" has to be asked in review, where 0117 could have
answered it with a parser. That is the honest cost of allowing the legitimate case.
## How it is checked
- **A module's own messaging needs no `requires`.** The check from 0117, unchanged: a module
declaring only `emits` and `consumes` reaches its subjects and is refused every other.
- **The broker holds no seat.** A manifest test: the broker module claims nothing, and a mesh
raised without it is complete — genesis names it nowhere.
- **`amqp` resolves like any provision.** A resolution test: a module requiring it is answered by
the provider, refused when none is assigned, and neither case touches the bus.
- **What cannot be checked mechanically**, and is said rather than implied: that a module holding
a broker is not using it to reach another module. Review, not a parser.
## References
- [ADR 0125](0125-the-bus-is-the-only-broker.md) — superseded; its ruling on the bus is kept
whole and only its ruling on the interface is reversed.
- [ADR 0106](0106-the-bus-is-nats.md) — the bus is NATS; its compatibility-broker framing is
corrected here.
- [ADR 0126](0126-a-module-declares-its-own-seats.md) — seats, including the one the NATS server
now holds alone.
@@ -0,0 +1,94 @@
---
topic: the mesh
status: accepted
date: 2026-09-27
deciders: jochen
reconstructed: false
supersedes: 0127-amqp-is-a-provision-not-the-bus.md
---
# 131. Everything on the mesh speaks to the broker seat, and AMQP is not a provision
## Context
[ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) settled the old broker as an ordinary provider
of an ordinary provision, `amqp`, kept for whatever wanted a message broker of its own. The day the
bus moved was the day that framing was tested, and it failed in a way that took the control plane
down for an evening.
Three things came out of the wreckage. **The protocol had leaked into the seat's contract**: for a
module to hold `mesh-broker`, it had to provide what the seat delivers, and what it delivered was
`amqp` — so the module that will carry the bus on NATS could not hold the seat that names the bus,
while the module the mesh was leaving could. **A consumer of `amqp` is not asking for AMQP.** The two
modules requiring it wanted the mesh's messaging — to emit an event, to hear a topic — and named the
wire protocol only because that was the word available. **And AMQP and NATS are not interchangeable
at the wire.** A provision named after a protocol can only ever be answered by that protocol, so once
the bus is NATS an `amqp` provision has one possible provider, and it is the thing being retired.
The operator's position, stated during the outage: modules depend on the broker *seat*, not on a
protocol; AMQP is obsolete as anything the mesh's core knows about; a module that depends on `amqp`
is wrong; and everything should reach the mesh's bus and be able to emit events and consume topics
through it.
## Decision
**A module that needs messaging uses the mesh's bus, and the mesh's bus is whatever holds
`mesh-broker`.** Emitting an event and consuming a topic go through the sdk, which is handed the
bus by the mesh with the module's own credential. No manifest names a wire protocol to get it.
**`amqp` is neither a provision nor a requirement.** Registration refuses a manifest that provides
it or requires it. The `mesh-broker` seat delivers `mesh-bus`, and its holder is the module that
provides `mesh-bus` — today the nats module, and only it.
**The old broker's module and the two modules that required it leave the catalogue.** They are
removed, not converted: one was a proof that a grant worked end to end, the other forwards mail off a
queue, and both are re-done against the bus if wanted, as new modules under this record.
**The controller's AMQP transport is deleted once every node reports on the new bus**, and the
switch that selects a transport goes with it — one bus, so nothing to select.
The predecessor's own broker is outside the mesh and not this record's concern
([ADR 0130](0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md)): what the predecessor's
tooling loses when it stops is accepted there.
## Options considered
1. **Keep 0127: AMQP stays an ordinary provision with the old broker as its provider.** Rejected. It
is what put the protocol into the seat's contract, it is why the seat could be left with no valid
holder mid-change, and it keeps two transports in the control plane indefinitely for the benefit of
two modules that did not want AMQP in the first place.
2. **Bridge it: the old broker's module also provides `mesh-bus`, so both can hold the seat during the
change.** Rejected. It makes the retiring broker a legitimate mesh bus for exactly as long as
nobody removes the line, which in practice is forever, and it leaves `amqp` as a thing the core
still knows the name of.
3. **The seat is the dependency; the protocol is nobody's business but the holder's.** Adopted.
## Consequences
- **The change of holder is a handover, and it needs a command.** Nothing today moves a seat from
one assignment to another as one act, and a seat the control plane dereferences cannot be empty
in between — that emptiness is the outage this record comes from. The command takes a seat and the
assignment taking it over. Designed and built before the cutover, under
[28 — Building the bus](../03-DESIGN/01-to-be/28-building-the-bus.md).
- **The seat's row moves to `mesh-bus` before the new holder registers, and that is safe.** The
control plane composes its own bus address through the seat *by name*
(`${seat:mesh-broker:…}`), and the overview derives holders by name; only registration and the
provision-to-seat resolution read what a seat delivers. So the row can change under the current
holder without unseating it, the new holder can then register its claim, and the handover happens
when both are running. Verified in the code during the outage, not assumed.
- **Registration gains two refusals**: a manifest providing `amqp`, and one requiring it.
- **The `rollout check` stops saying the old broker stays.** It said so under 0127; it now lists
unassigning it as the last step of the move.
- **What got harder**: a third party that genuinely wants an AMQP broker on a mesh node runs one as
any application module, with no provision and no seat, and nothing on the mesh routes to it. That
is the cost of the mesh not knowing the word.
## How this is checked
| Rule | Checked by |
|---|---|
| No manifest provides or requires `amqp` | a registration test refusing each, naming this record; and a whole-catalogue test asserting no registered manifest names it |
| `mesh-broker` delivers `mesh-bus`, and only a `mesh-bus` provider may hold it | the existing registration test for a delivering seat, with the row's value read from the store (mesh-controller#89) |
| The seat's row can change without unseating the holder | a test composing the control plane's own address and the overview under a row that the current holder does not satisfy |
| The rollout does not leave the old broker running | `rollout check` output, asserted in its test |
| The AMQP transport is gone | the package does not compile with it referenced; the switch variable is refused as unknown at start |
@@ -0,0 +1,142 @@
---
topic: the mesh
status: accepted
date: 2026-10-01
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
---
# 162. A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue
> **Superseded in part — 2026-10-10, by [ADR 0276](0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md).**
> "A merge produces a tiered plan" no longer holds: merges are collected in a rolling merge window, and each
> batch is planned once, as one walk with at most one commit per repository (issue 362). The plan the mesh
> keeps, its tiers and the one dependency relation stand as decided here.
> **Progressive insight — 2026-10-02.** The context below says a dependent is *built by whichever build machine is running — the only one there could be*. That was a fact of the day, not of the decision: since [ADR 0190](0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md) a tier's asks are taken by every machine holding the build seat. The plan and its tiers are unchanged.
> **Superseded in part — 2026-10-08, by [ADR 0267](0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md).**
> In decision 1, the `packages` kind ("the module's build reads the other's repository") and the *code*
> dependency ("B packages A's source … rebuilt whenever A changes, in the same tier") no longer hold: a file
> two modules build from moves both through their build sources, and no edge is drawn for sharing a
> repository. `stands-on` and `declared` name an artifact at each end. The build and runtime dependencies,
> `built-by`, and decisions 2 to 4 stand as decided here.
## Context
A merge on the forge reaches the controller as an event, and the controller asks the build
machine for what that merge changed. Until today that meant the modules whose recorded source is
that repository; since this afternoon it also means everything standing on what moved
([issue 186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md)).
Both are done inside the handler that received the event: it asks one build, waits for it, asks the
next, and returns when the last is done. Three things followed from that shape on 2026-10-01:
- The controller hears nothing else for the length of the work — twenty-five minutes for the runtime
image and its forty-three dependents ([issue 184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md)).
- A controller replaced mid-merge loses the rest of the merge: the redelivered announcement reads as
history, and the dependents are asked by hand.
- Nothing is deployed between builds. A merge that changes the build machine and something the build
machine builds asks for both in order, but the second is built by whichever build machine is running
— the old one, unless somebody pushed in between. The order the dependents are sorted in exists for
the artifacts; it says nothing about what must be *running*.
And the knowledge the order is computed from is scattered: a manifest's `build.on`, the artifacts a
build was made against, the repositories a build read, and the fact that every source-built module is
built by the build machine, each read by a different function in the merge handler.
## Decision
**1. A module's dependencies are one relation in the catalogue.** `depends-on` edges, each with the
kind of dependency on it: `stands-on` (the module's artifact is built on the other's), `packages` (the
module's build reads the other's repository), `built-by` (the module is built by the holder of the
build-machine seat), and `declared` (a manifest's `build.on`). The relation is answered by one query
of the catalogue — the controller's inventory today, the catalogue seat's tool when something outside
the controller needs it — and nothing else computes an edge. The edges are derived from facts
recorded at two moments and written by nobody: registration records the manifest (`declared`, and
`built-by` for anything with a source), a build's take-in records what the image was built on and
which repositories it read (`stands-on`, `packages`). A module's first build places it by its
declared edges alone; from its second it is placed by what was true.
**The kinds are three dependencies, not one.** A *code* dependency — B packages A's source — means
B is rebuilt whenever A changes, in the same tier: B's build needs nothing of A's first. A *build*
dependency — B stands on A's artifact, or declares it — means B is rebuilt after A is *built*, the
next tier, and nothing need be deployed in between. A *runtime* dependency — B is built by A — means
B is rebuilt only after A is built *and running*, the next tier with a gate on the machines' reports.
One cycle is real and resolved by the kinds themselves: the runtime image is built by the build
machine, and the build machine stands on the runtime image; the image comes first, built by the
build machine that is running, which is the only one there could be — a `built-by` edge never orders
a module after a build machine that stands on it. A provision is not a dependency of this relation:
a consumer binds to its provider through what the push renders, and a change to the provider's image
changes nothing in the consumer's; a consumer whose build does read a provider's source declares it.
"A was deployed, so restart B" is the push's domain — B is replaced when what it reads changed — and
not the plan's.
**2. A merge produces a plan, and the plan is a record.** The controller takes the modules the merge
changed and everything reachable from them along `depends-on` edges, and sorts that set into tiers:
tier 0 depends on nothing else in the set, tier 1 only on tier 0, and so on. The plan — the merge it
answers, the tiers, and each module's state — is written to the store before any build is asked. The
handler asks tier 0 and returns. Every build's outcome, taken in by the same handler that takes every
outcome in, advances the plan it belongs to; a controller replaced mid-plan resumes it from the store.
**3. A tier is done when it is built, and when what the next tier needs from it is running.** A
module whose roll-out policy says *roll out* is sent to its machines when it moves, as today. The next
tier is asked only once every module in this tier is built and every rolled-out module of this tier
that a later tier is `built-by` has been applied by the machines running it — the machines' reports
say so. A module whose policy says *record* is built and not waited for. So a merge
touching the build machine and the controller builds the build machine, waits until it is the build
machine that is running, and only then asks for the controller's build.
**4. A plan is read where the mesh is read.** `status` lists every open plan: the merge, the tier it
is at of how many, what it is waiting for and since when; `builds` lists the asked beside the built.
A plan that has waited past a bound is named red there, which is the first fact of
[issue 187](../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md)'s list.
## Consequences
- The merge handler returns in milliseconds; the receive loop is never held by a build again. Issue
184's remaining cause — a handler that waits for its own work — is removed rather than worked
around; the bus's heartbeats stop being dropped under a merge.
- A controller roll in the middle of a plan costs nothing: the plan is in the store and the asks are
in the queue (mesh-controller 194).
- A release across repositories is a plan whose edges cross repositories; the order a person kept in
a work-order file is the order the tiers give. Issue 186's third fault is answered by the plan,
not by a separate release record.
- The explicit `build --on <base>` stays as the way to ask for the same plan by hand.
- A module's `build.on` remains the one place a manifest states a dependency the store cannot see.
## How this is checked
| Rule | Checked by |
|---|---|
| Dependencies are one relation, each edge with its kind | an inventory test over a fixture catalogue: a runtime image, a module on it, a module packaging the controller's source, and the build machine; the four kinds come back from one call |
| A merge's set is sorted into tiers along the three kinds: a code dependency in the same tier, a build dependency after its base is built, a runtime dependency after the build machine; the build machine's own base first | a unit test on the tiering over the mesh's real shape: the runtime image, the build machine on it, modules built by it, a plugin declared on one, the proxy packaging the controller, an unrelated module left out; a cycle is one last tier and said |
| Only a runtime dependency gates on deployment, and only for a module that rolls out | the same test's gate cases |
| The plan is written before any build is asked, and the handler returns | a controller test: a merge announcement produces a plan row with its tiers and one asked build per tier-0 module, and the handler is back before any outcome |
| An outcome advances its plan; a complete tier asks the next; a tier with a rolled-out base waits for the machines' reports | store-backed tests over a two-tier plan: the first outcome marks built; the tier's roll-out gate holds until the report; the next tier is asked after |
| A controller restarted mid-plan resumes it | a test that opens a plan, drops the handler, and advances from the store alone |
| `status` lists open plans and names one that waits past the bound | the status JSON test with a fixture plan |
| Live | a catalogue merge touching a base and a dependent: the plan's tiers in `status`, the base rolled before the dependent is asked |
## Built and proven live, 2026-10-01
> **Progressive insight — 2026-10-01.** The decision stands; these are the facts of its building.
Built in mesh-controller 197 (the relation, the plan record, the driver, `status`), 198 (`plans`),
199 (a `built-by` edge orders and gates but never widens — the first live plan had taken the whole
catalogue along for a controller change; `plans stop`), 200. The first merge handled by the finished
machinery, at 18:56Z, was a controller change and produced the plan this record describes: tier 0
the build machine; tier 1 the controller and the proxy that packages its source. The handler
returned at once; the build machine was built, rolled, and the plan read *tier 0 built; waiting for
builder on novox to be applied* until the machine reported; then tier 1 was asked, both built, and
the plan read done — three minutes, read through the console with `plans`, the receive loop taking
reports throughout. What the day between decision and proof taught is in issues
[184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md),
[186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md) and
[188](../04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md).
## References
- [ADR 0157](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)
- [Design 30 — The mesh updates itself on a push](../03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md)
- Issues [184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md), [186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md), [187](../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md)
@@ -0,0 +1,275 @@
---
topic: what runs on it
status: accepted
date: 2026-10-07
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md
---
# 240. A module says how it is healthy, and the node-engine judges it
## Context
The release gate ([ADR 0236](0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md)
§2) judges a catalogue module on its first machine by what the mesh sees from outside: the build reported
applied, no witness put it back, no condition raised since the send about the machine or the module there,
and its tools served. None of that looks at what the module *runs*. To-be 45 names the gap in its Phase 4:
*container state in the node-engine's report, without which a container that crash-loops after its compose
applied is seen only through what it breaks.*
Measured on the live mesh for [research 032](../01-RESEARCH/032-a-module-says-how-it-is-healthy/00-overview.md)
([evidence](../01-RESEARCH/032-a-module-says-how-it-is-healthy/01-evidence.md)), 2026-10-07:
- **125 catalogue modules; 68 run something long-lived** — 49 a container that stays up, 19 a service
unit stated `running` and no such container. 50 run only their bundle in the node tools, 7 only files,
directories and packages.
- **No manifest can declare a health check, and nothing reads one.** The node-engine's report carries no
container or unit state; no probe of the self-check reads one.
- **19 of 73 long-running catalogue containers have an image that ships a check** (7 of 45 container
modules). The mesh never reads it. **Two of those were wrong in the mesh's hands**: the studio and the
flow editor read *unhealthy* while working, because their checks ask an address the program does not
bind in the mesh's configuration. Read without proof, they would have rolled back two good builds.
- **Every restart count is 0**, because a recreate loses it; the runtime's event history on the home
server is about a minute long, pushed out by its own check executions.
- **Of seven recent incidents**, liveness alone would have caught the agent server's crash loop (about a
hundred restarts, found by a person reading its log for something else); an HTTP readiness check the
web application that accepted TCP and answered nothing for eleven hours
([issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md));
only the module's own check the identity provider's refused administrator
([issue 179](../04-ISSUES/179-an-adopted-identity-providers-admin-never-took-the-minted-secret/00-report.md)).
A provisioner runtime that restarted until the overlay was up and then worked
([issue 058](../04-ISSUES/058-a-provisioner-runtime-crash-loops-until-the-overlay-is-up/00-report.md))
is the warning: a restart count without a start period reads churn that stops as a crash loop.
- **One sample is not a finding.** A single unanswered question raised an urgent alert nobody could read
([issue 277](../04-ISSUES/277-one-unanswered-question-was-an-urgent-alert-nobody-could-read/00-report.md));
the self-check now raises such a finding on the second look (to-be 45 §4).
The mesh now rolls a module out on its own and puts the previous build back when the first machine is not
healthy. That promise is only as good as "healthy" is, and for a module it is judged today from the outside.
**Checked against GENESIS.** *Failure must be loud* — a crash loop behind every passing check is work that
reported success and did nothing. *Anything requiring a human to notice it will be noticed late* — eleven
hours, and a hundred restarts, each found by a person. *Evidence over assertion* — "applied" is an
assertion about a declaration; "it answers" is a measurement. *The mesh notices when something is wrong
before you do* ([effect](../00-META/effect.md)). *Long-lived user services rather than an orchestrator*
([context](../00-META/context.md)) is why this record judges and reports, and restarts nothing on health:
an orchestrator's liveness restart is the part it does not take. *The mesh is a guest on a personal node*
bounds the cost: the checks' floors below. Nothing here conflicts with GENESIS.
## Considered Options
**Who runs the checks.**
1. *The container runtime's own check, read by the node-engine.* Rejected as the whole answer: containers
only — nothing for the 19 service-only modules or anything only a module's tool knows; every look is an
execution inside the container, which on the home server already pushes every lifecycle event out of
the runtime's history; an image check the module never stated is a check nobody owns, and two of 19 were
wrong. Kept as one *kind*, adopted by name and proved.
2. *The module's own tool answers "healthy", and that is the judgement.* Rejected as the judge: a bundle is
hosted by the node tools, so a tool that answers proves the bundle up, not the server; and a component
that judges itself is what [ADR 0227](0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md)
rule 8 refuses for the core. Kept as one kind, for function no endpoint shows, and only beside a check
the module does not run itself.
3. *The node-engine runs everything itself, including commands, on its own schedule.* Rejected in part: a
command that only makes sense inside the container is better timed by the runtime's own retries and
start period than by an execution per look from outside.
4. **The node-engine owns every check and every verdict, and runs each kind where it is cheapest.**
Chosen: HTTP, TCP and unit checks it makes itself; a command it hands to the runtime as that
container's check and reads; a tool it asks through the node tools. One runner and one reader per
machine, for every hosting form, and it keeps what the runtime forgets.
**Where the result goes.**
1. *Only a field of the report.* Rejected alone: a report follows an apply, so a container that goes bad at
03:00 waits for the next one.
2. *Only an event on each change.* Rejected alone: events are lost or replayed; an event is a sample, not
a state.
3. *A verb the controller calls per module when it judges.* Rejected: a pull per module per judging, and a
machine slow to answer reads as unhealthy.
4. **State in the report, its change on the bus, and a condition the controller raises on the second
look.** Chosen. It is how the core already says its own state; the gate, the self-check, the healers
([ADR 0231](0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md)) and
the operator's conversation ([ADR 0234](0234-the-mesh-holds-a-conversation-with-its-operator.md)) all
already act on conditions.
**What is judged.**
1. *Liveness only.* Rejected: no declarations needed, but it misses issue 145 (the port was open and the
program running) and issue 179.
2. *Readiness only, where declared.* Rejected: nothing is judged until every module has declared, and
liveness alone would have caught the worst incident of the window.
3. **Liveness for every long-running resource at once, readiness where declared, the declaration required
over a migration.** Chosen: the gate means something from the first build.
**What is done with an unhealthy module.**
1. *Restart it, as an orchestrator's liveness probe does.* Rejected for this record: the evidence holds no
case where a restart would have fixed anything — the crash loop was restarting already — and a restart
hides the failure the gate is meant to see. Whether a healer restarts what stays unhealthy is left to a
record of its own under ADR 0231.
2. **Nothing restarts on health; the condition reaches a person or a healer.** Chosen.
**A provider down.** With 12 consumers of the database provision and 36 of a route:
1. *Ignore it.* Rejected: twelve conditions for one fault, and twelve gates failed for something none of
them did.
2. *Judge providers first, consumers only once their providers are healthy.* Rejected: a consumer broken on
its own is not said while its provider is down, which is when it is most needed.
3. **A consumer's check names the provision it exercises; while that provision's provider is unhealthy on
the record, the consumer's finding is held under the provider's condition and its gate waits.** Chosen.
It is how [issue 281](../04-ISSUES/281-a-tier-sent-one-module-at-a-time-blamed-a-module-for-its-machine/00-report.md)
already treats a machine-level fault: what is the machine's is never pinned on a module.
**Where the declaration sits.**
1. *One per module.* Rejected: a module of eleven containers (the mail module) could not say which is wrong.
2. **On each long-running resource**, beside the resource's other fields. Chosen.
**The migration.**
1. *Required at once.* Rejected: 68 modules to change before the next merge, and nothing judged until all
are.
2. *Optional for ever.* Rejected: 38 of 45 container modules would stay at liveness, and a field that is
optional for ever is a field half the catalogue never gets.
3. **Liveness at once; the declaration required by a date, counted down.** Chosen.
## Decision
**1. Liveness is judged for every long-running resource, with no declaration.** A container that stays up,
a process that stays up and a service stated `running` are *alive* when running and not restarted more than
once within the settle window after their grace period. The node-engine observes this itself on each tick
and keeps the restarts it counted across recreates and across its own restarts; it never relies on the
runtime's restart count or event history. A resource held still by an open maintenance step
([ADR 0189](0189-the-store-keeps-what-the-records-name.md)) is neither alive nor dead: it is said as held,
and judged again when the step ends.
**2. A module declares how each long-running resource is ready**, in a field named `health` on that
resource: one kind — the image's own check adopted by name, an HTTP request to a declared endpoint and the
status expected, a TCP connect to a declared endpoint, a command in the container, the unit's own
readiness, or one of the module's tools — with an interval (default 30 s, **not under 10 s**), a timeout
under the interval, a number of failing looks in a row before it is unhealthy (default 3, **not under 2**),
and a grace period after a start (default 60 s), in which failure does not count. The grace and the failing
looks together are at most five minutes, so a resource broken from its start is said within the gate's
bound. An endpoint is named by its `listens` name, never by a port or an address, so a check follows the
machine's ports as the endpoint does. A check of function that no endpoint shows is a module's own tool, and
only in addition to a check the module does not run itself.
**3. The node-engine runs every check and owns every verdict.** HTTP, TCP and unit checks it makes itself;
a command in the container it hands to the runtime as that container's check and reads the state; an
adopted image check it reads the same way; a tool it asks through the node tools. Nothing else on the machine
judges a module, and nothing else sets a container's check.
**4. The state goes in the report, its change on the bus, and a condition is the controller's.** Each
report carries, per module and long-running resource, a state — healthy, unhealthy, starting, held,
unknown — since when, the failing streak and the restarts counted; each change is emitted as an event, and a
state that is not healthy is said again while it lasts. The controller keeps the last state per machine,
raises `module.<module>.<machine>.unhealthy` when two consecutive statements say so, and clears it on the
first that does not. **The gate needs no new rule:** ADR 0236 §2's *a module's own health holds* now reads
the module's stated health — a judging is healthy only when every long-running resource of the module on that
machine is healthy, so a resource still starting is not yet a pass — and its *no condition raised since the
send* holds the module on this condition. Every start begins in `starting`, so a condition from before the
send clears at the new build's start and anything after it is the new build's. At the gate's bound the
build is put back, as ADR 0236 §3 says.
> **The mechanism changed — 2026-10-10, by [ADR 0283](0283-a-part-that-waits-for-the-operators-secret-or-setting-is-said-as-waiting-passes-the-first-node-gate-and-names-the-act.md).** The four states, the
> condition and the gate's reading stand. A fifth state, `waiting`, now sits beside them: a tool check
> (rule 2) may answer *waits* as well as healthy or not, naming an own secret or a setting the operator has
> yet to give, and one look that says so is enough, inside the grace too. The controller checks the wait,
> says it as `needs-operator` rather than `unhealthy`, and the gate reads it as ADR 0254's *waits for a
> person*: a pass.
**5. A provider down is said once, at the provider.** A check names the provision it exercises. While
that provision's provider for this consumer is unhealthy on the record, the consumer's finding is held under
the provider's condition — listed there as waiting on it, raised as nothing of its own — and the consumer's
gate waits rather than fails. What a consumer finds while its provider is healthy is its own.
> **The mechanism changed — 2026-10-11, by [ADR 0292](0292-a-consumer-of-the-part-a-provider-waits-on-is-held-under-the-wait-and-passes-its-gate-as-a-wait-for-a-person.md).** The hold under an unhealthy provider, and its consumer's gate
> waiting, stand. A provider that only waits for the operator (ADR 0283) now holds the consumers of a provision one
> of its waits covers, listed at its `needs-operator` condition, which stays a warning; their gate passes as a wait
> for a person, carrying the wait. A consumer no wait covers is said on its own.
**6. Nothing is restarted for being unhealthy.** The runtime restarts what exits, as now. A condition
reaches a person through the operator's conversation, or a healer; a healer that restarts on health is its
own decision.
**7. A declaration is proved before it is trusted.** `module check` refuses a `health` field that names an
endpoint the module does not declare, an interval, timeout or count outside its bounds, or a tool the module
does not serve. A bed from mesh-lab, run by the catalogue's check on the build seat, starts every resource
whose declaration or image changed and requires its check to say healthy within its grace; an image check
adopted by name is proved the same way, because two of nineteen were wrong. This proves the check's
wiring — that it can see the program working — not the change, which the live mesh and the first machine's
gate still judge ([ADR 0149](0149-the-live-mesh-is-the-test-bed.md) stands).
**8. Every catalogue module that runs something long-lived declares one.** A catalogue-wide count of the
long-running resources without `health` may only go down. `module check` warns from this decision, and
refuses a long-running resource without `health` once the count reaches zero, or six weeks after liveness is
first judged live, whichever is first. A module running nothing long-lived — its bundle only, or files and
packages — declares none: the node tools serving its tools is its liveness, as the gate judges today.
## Consequences
- **Liveness alone, from the first build, would have caught the crash loop inside the gate's ten minutes.**
Readiness would have caught the silent web application in about a minute instead of eleven hours. The
identity provider's refused administrator needs the module's own tool, which it already has (ADR 0224 §5).
- **The node-engine grows a small scheduler, a field of its report and an event; the controller a condition
kind and its two-look rule; the gate a reading of state it already had a place for.** A machine of 45
containers spends tens of milliseconds a look reading state, and about 3 s a minute on in-container
commands at the default interval.
- **A module whose first machine was unhealthy before the send no longer passes the gate for being
unchanged in its unhealthiness** — the start resets it to `starting`, and the new build is judged on its
own.
- **What a check names becomes load-bearing.** A check naming the wrong provision hides a consumer's own
fault under its provider; the bed and the first machine's gate are what catch that.
- **Harder:** a slow starter — a module that needs more than five minutes to be ready — cannot say so
within these bounds and fails its gate. That is accepted until one exists; it would be a change to the
bound, recorded.
- **A provider's health and its consumers' failures meet in two places**: this record's condition (the
provider's resource is unhealthy) and ADR 0224's standing (the provider keeps failing a consumer). They
are different facts, both kept; the hold of rule 5 reads only this record's.
- **Data is not health.** Whether a module's data is there, measured and backed up stays
[ADR 0233](0233-a-module-declares-the-data-it-holds-and-the-mesh-protects-and-watches-it-from-that.md)'s,
watched by the self-check's D13.
## What this does not decide
- Whether a healer restarts what stays unhealthy (rule 6 leaves it to its own record, under ADR 0231).
- Health for scheduled work — whether the last scheduled run succeeded belongs to the record of the
scheduled step.
- Health of what a module's events do ([issue 276](../04-ISSUES/276-a-handler-that-did-its-work-was-offered-it-five-times/00-report.md))
— the event contract's, and the bus watchdog's.
- The core's own definitions (ADR 0236 §1, to-be 45 §8), which stand; a core component may later declare its
own through the same field.
## How it is checked
| Rule | Checked by |
|---|---|
| 1 Liveness without declaration | node-engine tests over a fake runtime and service manager: a container recreated keeps its counted restarts, and so does the engine restarted; a restart inside grace is not counted; two restarts within the settle window after grace make it unhealthy; a resource under a maintenance step is held. A mesh-lab replay of the crash loop (a container whose program exits at start) fails its gate within the bound |
| 2 The field and its bounds | `module check` refuses each out-of-range part and each endpoint named by port or address, a test per refusal; a catalogue-wide test parses every `health` field |
| 3 The engine owns the verdict | a node-engine test that a declared command becomes the container's check and nothing else sets one; a test that an HTTP check dials the endpoint's current port after a port change |
| 4 Report, event, condition, gate | controller tests: one unhealthy statement raises nothing and is listed unconfirmed; two raise; a healthy one clears; the gate fails a judging while a resource is starting or unhealthy and holds a module on this condition (the existing gate test, extended); a condition from before the send clears at the new build's start. A replay of issue 145 — a database made unreachable — raises the consumer's condition within two looks |
| 5 Said once at the provider | a controller test with one unhealthy database provider and three consumers failing: one condition, at the provider, the consumers listed as waiting; their gates wait, not fail; a consumer failing while its provider is healthy is raised on its own |
| 6 No restart on health | a node-engine test that an unhealthy container is not restarted, recreated or stopped |
| 7 Proved before trusted | the bed's run of every changed declaration in the catalogue's check; the replay of the studio's false *unhealthy* (a check that asks an address the program does not bind) fails the bed, not a machine |
| 8 Every long-running module declares | the catalogue-wide count of long-running resources without `health`, compared with the number kept in the catalogue: a merge may lower it and never raise it; after the date, `module check` refuses |
| live | every machine's report carries a state for every long-running resource; `conditions` raises `module.<module>.<machine>.unhealthy` for a module stopped on purpose and clears it when it runs |
## References
- [Research 032](../01-RESEARCH/032-a-module-says-how-it-is-healthy/00-overview.md) — the evidence, the
options and the recommendation this record takes.
- [To-be 48](../03-DESIGN/01-to-be/48-a-module-says-how-it-is-healthy.md), the design.
- [ADR 0236](0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md)
§2, whose module health this gives a content;
[ADR 0227](0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md) rules 5, 6
and 8 and [to-be 45](../03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md) §2, §4 and §8 — the
condition, the second look, the witness that is never the component;
[ADR 0224](0224-a-provider-that-keeps-failing-a-consumer-is-a-problem-the-controller-reports.md),
[ADR 0231](0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md),
[ADR 0233](0233-a-module-declares-the-data-it-holds-and-the-mesh-protects-and-watches-it-from-that.md),
[ADR 0234](0234-the-mesh-holds-a-conversation-with-its-operator.md),
[ADR 0189](0189-the-store-keeps-what-the-records-name.md),
[ADR 0149](0149-the-live-mesh-is-the-test-bed.md),
[ADR 0237](0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md).
- Issues 058, 145, 179, 181, 276, 277, 281.
@@ -0,0 +1,99 @@
---
topic: the mesh
status: accepted
date: 2026-10-08
deciders: jochen
reconstructed: false
supersedes-in-part:
- 02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md
extends: 02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md
---
# 249. A delivery group's order rules have a precedence, and a declared order outranks them all
## Context
[ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)
decision 4 orders a delivery group's members by a line `after: <repository>` and by three inferred rules:
*built by* (a member that moves a module goes before a member whose modules are built by it or stand on
it), *version skew* (the controller before a manifest change) and *engine before controller* (the
node-engine before the controller, from ADR 0236's order for its walks). It says nothing about two rules
ordering one pair in opposite directions. The planner treated every such pair as a cycle, and a declared
order against an inferred one as a cycle too.
On 2026-10-08 the group `feat/a-machine-joins-through-the-tunnel` held three pull requests, each `ready`
on its own: the controller's, the node-engine's and the lab's
([issue 309](../04-ISSUES/309-two-order-rules-ordered-one-pair-both-ways/00-report.md)). The controller's
repository also holds the build agent, and its pull request moved it. Every source-built module is built
by the build agent, so *built by* put the controller's member first. *Engine before controller* put the
node-engine's member first. The group was rejected: "its order contradicts itself", naming the two
members and neither rule. The person merged it by hand, the controller first, since that member only added
a verb the new node-engine calls. That is the order *built by* gave, and the order
[ADR 0246](0246-a-seats-new-verb-is-promised-before-it-is-required.md) asks for: a verb is promised before
it is required.
The rules are not of one kind. *Built by* and *version skew* are read from the graph and from what the
change does: which module builds which, and whether a manifest changes. *Engine before controller* is
read from two module names alone. It holds when the controller starts sending something the node-engine
must first accept. It is wrong when the node-engine starts asking for something the controller must first
serve, as here. The rule cannot tell the two apart.
## Considered Options
1. **A precedence among the rules, and a declared order above them all.** Chosen.
2. **Narrow *built by* so it never applies through the build agent to the node-engine.** It would end
this case. But a builder that changes how the node-engine is built does have to go first, and the next
pair of rules that disagree would be refused the same way. Rejected.
3. **Drop *engine before controller*.** ADR 0236's reason still holds when the node-engine is what must
accept something new. Rejected.
4. **Keep refusing, and name the two rules.** Then every group that moves the controller's repository and
the node-engine together is refused, and the person must still merge by hand. A declared order could not
help, because a declared order against an inferred one was also a cycle. Rejected.
## Decision
1. **When rules order one pair of members in opposite directions, the rule of higher precedence decides.**
From highest to lowest:
- *declared*: an `after:` line a person wrote;
- *built by* and *version skew*: what the graph and the change say;
- *engine before controller*: the default for walks, read from module names alone.
2. **A declared order outranks every inferred rule.** This replaces ADR 0239 decision 4's sentence "a
declared order that contradicts an inferred one is a cycle". A person who knows the order the rules
cannot see writes one line, and the group follows it.
3. **The order says which rule won.** Each pair carries its rule and the rules it won over. The delivery's
`show`, `groups` and plan note print them, for example "the controller's member before the node-engine's:
built by, over engine before controller".
4. **Rules of one rank in opposite directions are still refused.** Two `after:` lines naming each other,
or *built by* both ways, have no precedence between them. The group is rejected. The reason names both
members, both rules and the direction each gives, and says how to declare the order. A cycle through
three members or more is refused naming the pairs in it.
## Consequences
- The group of issue 309 is ordered without a person: the controller's member first.
- A group can no longer be refused because a person's `after:` disagrees with an inferred rule. The person
can now overrule *version skew*, and a manifest change merged before the controller that parses it fails
at the gate, not silently. The rule the person overruled is printed on the pair.
- *Engine before controller* now decides only where nothing in the graph or the pull requests does.
- The ranks are a list in the controller's planner. A new inferred rule must be given a rank when it is
added.
**How it is checked.** The controller's tests `TestAnOrderRuleYieldsToAStrongerOne` and
`TestAGroupIsOrderedByTheGraphAndWhatItsPullRequestsSay` check that *built by* wins over *engine before
controller*, that a declared line wins over both and over *version skew*, that each pair names what it won
over, and that equal-rank rules both ways are refused with both rules named and the `after:` lines to
write. mesh-delivery's `TestAGroupSaysWhichOrderRuleWonOrWhyNoneDid` and
`TestACycleWithoutAContradictionNamesItsPairs` check that the refusal and the won-over rules reach
`groups` and the group's state. The replay R309 in mesh-lab's register fails before the fix and passes on it.
## References
- [Issue 309](../04-ISSUES/309-two-order-rules-ordered-one-pair-both-ways/00-report.md), its report and
diagnosis.
- [ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)
decision 4, the sentence this replaces.
- [ADR 0236](0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md),
*Rollout order*, where *engine before controller* comes from.
- [ADR 0246](0246-a-seats-new-verb-is-promised-before-it-is-required.md), a verb promised before it is
required.
- [Design 47](../03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md), *A delivery group*.
@@ -0,0 +1,249 @@
---
topic: the mesh
status: accepted
date: 2026-10-10
deciders: jochen
reconstructed: false
supersedes-in-part:
- 02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md
- 02-DECISIONS/0218-a-plan-sends-grants-before-code-rolls-out-one-machine-first-and-a-newer-merge-takes-over-an-older-plan.md
- 02-DECISIONS/0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md
- 02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md
extends: 02-DECISIONS/0249-a-delivery-groups-order-rules-have-a-precedence-and-a-declared-order-outranks-them-all.md
---
# 276. Merges are assembled in a rolling window, and each batch is delivered by one walk with one commit per repository
> **Superseded in part — 2026-10-10, by [ADR 0282](0282-a-fix-to-a-leaf-module-runs-healthy-on-every-node-within-five-minutes-of-its-merge-and-a-core-module-within-ten.md).**
> Decision 4's "one walk at a time", and decision 1's window for every merge, no longer hold: a lone merge is
> cut at once, and walks that share no repository, module or edge of the module graph run at once, one
> first-node judgement per node at a time. Two walks of one repository never at once, no walk superseded once
> it started, and the window for a merge that waits for its delivery group or arrives while a batch assembles
> stand as decided here.
## Context
Shortly after midnight on 2026-10-10 two pull requests of the catalogue were merged 18 seconds apart, the
claude-code change at 00:20:52 and the dunst change at 00:21:09
([issue 362](../04-ISSUES/362-two-merges-seconds-apart-left-their-delivery-waiting-for-a-person/00-report.md)).
The controller opened one walk for each merge, as [ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md)
decided ("a merge produces a tiered plan"). The second walk superseded the first, as
[ADR 0218](0218-a-plan-sends-grants-before-code-rolls-out-one-machine-first-and-a-newer-merge-takes-over-an-older-plan.md)
decision 3 decided, and both were planned at the dunst commit, as the fix for
[issue 349](../04-ISSUES/349-a-merge-acted-on-late-superseded-the-merge-after-it-and-built-the-older-commit/00-report.md)
decided. mesh-delivery then tied the surviving walk to the dunst delivery, which the supersession had already
ended, and tied no walk to the claude-code delivery. Nothing gave the walk its word. The change waited 58
minutes, until the operator started the walk by hand with `plans go`.
The diagnosis found two parts that each behave as written and disagree on what a walk's commit means. That
is the narrow cause. The wide one is that the mesh plans one walk per merge, while merges arrive in bursts:
- **A burst is the normal case.** The operator merges a batch of reviewed pull requests at once, often
across repositories (a catalogue change with the controller change it needs). Every merge opened a walk,
and every walk after the first superseded the one before. Three supersession paths now exist (ADR 0218,
issue 254, issue 349), each with its own bookkeeping, and issue 362 is a fourth way for them to disagree.
- **A walk superseded mid-gate wastes the gate and confuses its record.** A first machine judged healthy
for a build that the next walk replaces a minute later proves nothing about the build that stays. On
2026-10-09 a controller merge landing while another walk was at its gate rolled everything back; the rule
"one controller rollout at a time" was kept since then by the person merging, not by the mesh.
- **ADR 0239 made a delivery "one commit … from commit to delivered" and walked it alone**, while its own
decision 4 delivers a delivery group across repositories as one unit. The walk the commit gets is
decided by when the controller heard its merge, which is not something the person merging controls.
On 2026-10-10 the operator decided the model, in their words:
> "a delivery doesn't really 'run from commit to delivered', we've already discussed that a delivery plan
> might start with a set of commits spanning multiple repo's, however, only 1 commit per repo is allowed
> … we assemble all commits that are coming in with a 1 minute rolling window, so if a new commit comes
> in, less then 1 or 2 minutes after the last one, the builder again waits 1 or 2 minutes, until no new
> commit came in, then it looks at the 'group of commits' and decides if they all fit into 1 delivery
> plan … This is a hard problem to tackle, but it will improve our build setup I believe."
> "the 'actively assembling plan' is very interesting, the controller can say 'Still waiting for 13
> seconds for new commits, these are the currently grouped commits, plan is yet to be calculated'."
On two commits of one repository in one window, the operator chose the later commit over splitting:
"i like your idea better, I just thought it would be more complex, but if it's reasonably doable, go for
it".
**Checked against GENESIS.** *Failure must be loud*: an assembling batch and a waiting walk each have a
bound and a condition. *The mesh owns the truth*: the window, the batch and the walk are kept by the
controller and shown by its `plans`, not inferred from timing by a person. Nothing here conflicts with
GENESIS.
**Words.** The glossary had no word for the merges one window collects, nor for the window. This record
adds **merge window** and **batch** to it (Change and delivery), with this record. *Group* is not used for
either, because **delivery group** already names deliveries that share a head branch name.
## Considered Options
**What a walk answers.**
1. **One walk per merge, as today, with the bookkeeping repaired.** The walk names the merges it answers,
and mesh-delivery ties a superseding walk to every live delivery it contains. Rejected as the model,
kept as a repair (decision 8): it makes the 362 shape deliver, but every burst still opens and
supersedes walks, a walk can still be superseded at its gate, and a cross-repository change is still
walked as separate walks in whatever order the merges were heard.
2. **A rolling merge window, and one walk per batch** (chosen). Merges are collected while they keep
arriving; the batch they form is planned once, as one walk with at most one commit per repository.
3. **Every walk started by hand.** The operator named it as the alternative ("this does mean we need to
manually start all delivery plans, or, even better, …"). Rejected: a person in every delivery is the
wait ADR 0239 exists to remove, and a merge that passed its checks must reach its machines without a
person.
**Two commits of one repository in one batch.**
1. **Split into two walks in merge order** (the operator's first proposal). The second walk waits for the
first. Rejected: every machine running the module builds, is sent and is judged on the earlier commit,
and then again on the later one minutes after; the gate judges a build that is about to be replaced;
and the batch's walk stops being one unit.
2. **One walk with the later commit** (chosen). On a linear trunk the later merge commit contains the
earlier one, so building it delivers both. One build, one gate, one rollout. The earlier merge is
answered by its successor and ends `covered` when the walk is delivered. Only when that walk fails are
the earlier merges walked on their own, to find which one brought the failure.
**Who keeps the window.**
1. **mesh-delivery**, which owns deliveries (ADR 0239). Rejected: a walk on the controller's own path (the
controller, the node-engine, the node tools, the bus, mesh-delivery itself) must not wait for
mesh-delivery, and with no holder of the seat on record the controller plans alone. The window would
then exist twice, once in each.
2. **The controller** (chosen). It hears every merge (the bus, and the catch-up that reads back what the
bus missed), it owns the planner and the walk, and it already plans alone for its own path. mesh-delivery
keeps what it owns: each commit's delivery record, the order of a delivery group, and the word that lets
a waiting walk start.
## Decision
1. **A rolling merge window.** The controller collects every merge into a branch a module follows into the
open **batch**. The window is rolling: each merge restarts it. It closes when no merge arrived for the
window's length, 90 seconds by default (the controller's setting `merge-window`), and at the latest 10
minutes after the batch's first merge (`merge-window-at-most`), so a busy period never starves delivery.
The batch, its merges and its times are kept in the controller's store, so a restarted controller
resumes the window where it stood rather than losing or doubling a merge.
2. **A batch is planned once, as one walk with at most one commit per repository.** When the window closes,
the planner computes one delivery plan for the batch: every module its commits move, their dependents,
in tiers along the module graph, across repositories. For a repository with two or more merges in the
batch the walk takes the latest commit of its branch, which contains the earlier ones. **Every merge
belongs to exactly one walk**: the walk names every merge it answers, its own commits and the earlier
ones they contain. A merge is never dropped and never walked twice.
3. **The earlier merge of a repository is answered by its successor.** Its delivery follows the walk and
ends `covered` when the walk is delivered, naming the merge that carried it (the glossary's word,
[ADR 0270](0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md);
here the containment is the trunk's order inside one batch, so the forge is not asked). **When that walk
fails**, the walk's own commits fail, and each earlier merge it answered is walked on its own commit, the
newest first, one walk each, in the next batches: the first that is delivered ends `delivered`, and the
search stops there. A walk that failed never marks a merge delivered.
4. **One walk at a time, and nothing supersedes a walk that started.** A batch becomes a walk only when no
walk is open. Merges that arrive while a walk runs join the next batch, whatever its window says. A walk
that has not started, because it waits for mesh-delivery's word, is folded into the next batch when that
batch is planned, and the new walk answers every merge the folded one answered. This is the only
supersession left; ADR 0218 decision 3 and the issue 349 rule of planning a late merge at a later commit
are replaced by it. A merge heard after a later merge of its repository and branch was put in a walk is
answered by that walk: by the batch still assembling, by the open walk, or, when that walk was delivered,
it ends `covered` at once.
> **The mechanism changed — 2026-10-10, by [ADR 0282](0282-a-fix-to-a-leaf-module-runs-healthy-on-every-node-within-five-minutes-of-its-merge-and-a-core-module-within-ten.md).**
> What stands: the window for a merge whose delivery group has a member not yet merged or that arrives while a
> batch assembles (decision 1), two walks of one repository never at once, and no walk superseded once it
> started. What moved: a lone merge is cut at once without the window, and walks that share no repository,
> module or module-graph edge run at once, one first-node judgement per machine at a time. Until ADR 0282's
> second and fourth slices are built, the window for every merge and one walk at a time hold.
5. **The order of a delivery group holds inside a walk** (ADR 0249 unchanged). The pairs its order rules
give (declared, built by, version skew, engine before controller, each with the rule that won) become
edges of the walk's tiers beside the module graph: the modules of a member ordered after another are in
a later tier than that member's. A contradiction still rejects the group before anything merges.
mesh-delivery gives a walk its word only when every delivery it carries is at its turn.
6. **A delivery is one commit's record, and its walk is the batch's.** A delivery keeps its pull request,
its status, its note and its check, as ADR 0239 decided. From its merge on it is delivered by the walk
that answers it, and every delivery a walk carries ends with that walk's result: `delivered` (its own
commit) or `covered` (an earlier commit of the same repository) when the walk is done, `failed` when it
fails, `stopped` when it is stopped. The walk's machine steps are recorded on each delivery it carries.
7. **The assembling state is shown.** The controller's `plans` lists the open batch first, as the operator
asked: "assembling: 13 s left (at the latest 00:31:09); grouped: novox/mesh-catalog@48bda475 (answers
553b7191), novox/mesh-controller@a6bc0931; plan not yet calculated". It says the batch on the bus as
`plan-moved` with the state `assembling`, so mesh-delivery's `deliveries` and `show` say of a merged
delivery that its batch is assembling and how long is left, and of one whose batch waits that it waits
behind which walk.
8. **The bookkeeping is repaired as well** (the narrow cause of issue 362), because decision 4 keeps one
supersession path. A walk names every merge it answers, and a superseded walk names the walk that took it
over. mesh-delivery ties a walk to every live delivery whose merge it answers, follows a walk to the one
that took it over, and **never ties a walk to a delivery that has ended**. A walk waiting for the word
that no live delivery holds is either adopted as a delivery of its own or listed at once by `stalled`.
9. **Every wait has a bound and a condition.** A batch still assembling a minute past
`merge-window-at-most` while no walk is open raises a condition: the controller failed to cut it. A
batch waiting behind an open walk longer than that walk's bound raises one naming the walk. A walk
waiting for its word past 30 minutes is S16's, as before, and its words name the merges it answers. A
merged delivery no walk answers within ten minutes is held with the true reason (which open walks of its
repository exist and that none names its merge, or that the controller holds none) and listed by
`stalled` at once, so probe D14 raises it for the operator.
> **Decided during the build, 2026-10-10** (on the operator's delegation). Decision 2 batches every merge of the
> window together, and decision 4 starts a walk on the controller's own path without mesh-delivery's word. A
> batch holding both a merge on that path and a catalogue merge would therefore start the catalogue's deliveries
> without their word: their turn in a delivery group, their holds, and the order of decision 5 would be skipped.
> So **a merge on the controller's own path never shares a batch with a merge that waits for the delivery's
> word.** Such a merge goes into a batch of its own, with its own window; the open catalogue batch closes and is
> cut by the normal rules, and queues behind the own-path walk like any other. Catalogue merges heard during the
> own-path batch's window join the next catalogue batch. An own-path walk being cut folds no catalogue walk that
> waits for its word: that walk's merges are batched again behind it, and the walk names that batch as the one
> that took it over, so no catalogue delivery ever skips its turn. Checked by the controller's test
> `TestAMergeOnTheControllersPathNeverSharesABatch`.
## Consequences
- **A merge waits up to 90 seconds before it is planned**, and up to 10 minutes in a busy period. A walk
takes minutes to build and longer to judge, so the wait is small beside it, and it removes the builds,
sends and gates that a superseded walk wasted.
- **A cross-repository change merged within a window is walked as one unit**, tiered by the graph and its
group's order. A person no longer has to merge in order and wait in between.
- **One walk at a time is mechanical.** A controller merge can no longer land in the middle of another
walk's gate; it waits for the next batch.
- **A failing walk fails every delivery it carried**, including one whose own modules passed before the
failure. Each delivery's machine steps say how far it got, and decision 3's search finds the culprit only
among the merges of one repository. A failure in a walk that carried several repositories is the
operator's to read. This is the price of one unit, and the operator chose it.
- **What got harder**: the controller's merge handling holds state across merges (the open batch) and is
restart-safe; the walk's record names several repositories and commits instead of one, and every reader of
it (`plans`, S3, S16, `plan-moved`, mesh-delivery) changes with it. The store gains the batch and the
merges a walk answers, as a numbered migration and in the consolidated schema.
> **Progressive insight — 2026-10-10.** The controller has no consolidated schema file: a fresh install runs its
> numbered migrations from the first, embedded in the build, so the numbered migration (0089) is the whole of
> the schema change. The bullet above said "as a numbered migration and in the consolidated schema"; that second
> half describes mesh-delivery's kind of store, not the controller's. The decision stands.
- **The replay of issue 362** in the lab raises a mesh, merges two commits of one repository 18 seconds
apart, and passes when one walk delivers both without a person; it fails on the commit before the change.
## How it is checked
- The controller's tests: two merges of one repository 18 seconds apart make one batch and one walk at the
later commit, answering both; merges in three repositories within the window make one walk; a merge at
second 80 restarts the window; the window closes at `merge-window-at-most` however busy; a merge arriving
while a walk runs joins the next batch; a walk that has not started is folded into the next one; `plans`
shows the assembling state; the batch survives a restart; the two conditions of decision 9 are raised past
their bounds and not before.
- mesh-delivery's tests: both deliveries of one walk end `delivered` and `covered` with its done record; a
failed walk marks none delivered and walks the earlier merge alone; a walk is never tied to an ended
delivery; a walk is given its word only when every delivery it carries is at its turn; a group's order is
unchanged; a held delivery with no walk names the true reason and is listed by `stalled`.
- Each of those tests fails with its part of the change removed.
- mesh-lab's replay R362 (decision's consequence above), proved to fail before the change and pass after it.
## References
- [Issue 362](../04-ISSUES/362-two-merges-seconds-apart-left-their-delivery-waiting-for-a-person/00-report.md),
its diagnosis, and the replay R362 in mesh-lab.
- [ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md) (a merge produces a plan),
[ADR 0218](0218-a-plan-sends-grants-before-code-rolls-out-one-machine-first-and-a-newer-merge-takes-over-an-older-plan.md)
decision 3 (a newer merge takes over an older plan), [ADR 0238](0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md)
decision 2 (one commit, one change plan), [ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)
(a delivery walked by its own commit's walk), each superseded in the part this record names.
- [ADR 0249](0249-a-delivery-groups-order-rules-have-a-precedence-and-a-declared-order-outranks-them-all.md),
whose order rules this record keeps and uses as tier edges.
- [ADR 0270](0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md),
whose `covered` ends an earlier merge of a repository.
- [Issue 349](../04-ISSUES/349-a-merge-acted-on-late-superseded-the-merge-after-it-and-built-the-older-commit/00-report.md),
whose rule decision 4 replaces.
- To-be [47](../03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md) and
[10](../03-DESIGN/01-to-be/10-delivery.md), amended with this record.
@@ -0,0 +1,236 @@
---
topic: the mesh
status: accepted
date: 2026-10-10
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md
supersedes-in-part:
- 02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md
---
# 282. A fix to a leaf module runs healthy on every node within five minutes of its merge, and a core module within ten
## Context
On 2026-10-10 the operator asked, in their words: "we definitely need to speed up our bug fixing process so a
bugfix can land deployed and rolled out in less than 5 minutes". Issue 382 in the issue tracker (a small fix took
45 minutes to reach a node) names the causes it saw: a 90-second merge window, one walk at a time, about four to
five minutes per tier, and three judgement passes for every module. Issue 338 (a module built from a shared
repository moves on every merge to it) names a fourth: every merge of the controller walks two catalogue modules
in two more tiers.
### What was measured
Every walk the controller keeps (`delivery-walks`) records, per module, when its build was asked and finished,
when its first node was sent it, when the first-node gate judged it and when the rest were sent. Read on
2026-10-10 for the 51 walks that finished between 2026-10-09 13:55 and 2026-10-10 17:32 (UTC), each walk's time
from its merge to its builds being sent to every node it targets splits into these phases. A walk is a **core**
walk when it moves a module of the controller's own path (the controller, the node-engine, the tool runner, the
bus, mesh-delivery) or the mesh's resolver, and a **leaf** walk otherwise. Nine walks that failed or were folded
into another are not counted.
| Phase, in minutes | Leaf: median | Leaf: worst | Leaf: share | Core: median | Core: worst | Core: share |
|---|---|---|---|---|---|---|
| the merge window: merge to walk opened (a wait over 2.2 counted as 1.8 of window, the rest as waiting) | 0.5 | 2.1 | 9 % | 0.4 | 2.1 | 7 % |
| waiting behind another walk | 0.3 | 59.1 | 47 % | 0.0 | 1.8 | 1 % |
| build | 0.4 | 1.6 | 5 % | 1.2 | 4.1 | 14 % |
| built to sent to the first node | 0.3 | 0.6 | 3 % | 0.5 | 3.0 | 5 % |
| first-node judgement, the longest of each tier | 2.6 | 6.3 | 27 % | 7.2 | 14.9 | 63 % |
| first nodes of one tier judged one after another | 0.0 | 4.2 | 8 % | 0.0 | 0.0 | 0 % |
| judged to sent to the rest | 0.0 | 0.9 | 2 % | 0.5 | 5.1 | 6 % |
| between tiers | 0.0 | 0.6 | 0 % | 0.0 | 1.0 | 3 % |
| **merge to sent to every node** | **6.0** | **66.0** | | **9.7** | **20.5** | |
- **Leaf walks** (20): median 6.0 minutes, 90th percentile 14.8, worst 66.0; **7 of 20 within five minutes**.
The worst is the incident of issue 362 (58 minutes waiting for a person's word), which ADR 0276 already
repaired; without it the worst wait behind another walk is 13.3 minutes. Since the merge window went live on
2026-10-10 at about 12:40, a lone merge waits 1.6 to 2.1 minutes for its window (median 1.8), and **4 of the 9
leaf walks since then waited behind another walk**, for 1.8, 4.9, 9.5 and 13.3 minutes, each behind a walk of
the controller's own path.
- **Core walks** (31): median 9.7, 90th percentile 17.0, worst 20.5; 4 of 31 within five minutes. They are two
populations. **13 core walks had one tier**: median 5.6, 90th percentile 7.2, worst 8.8. **18 walks
were controller merges that also walked the build agent and the route proxy** in tiers of their own (issue
338): median 14.2, worst 20.5, none within ten minutes.
- **The first-node judgement** is three healthy readings, at least forty seconds apart and two minutes after
the send (ADR 0236 decision 2). It took a median of 2.4 minutes for a core module other than the controller (10
judgements), 3.6 minutes for the controller (19 judgements, worst 4.3, its readiness includes its
self-check), and 2.6 minutes per tier of a leaf walk. **Five of the 20 leaf walks judged two first nodes one
after the other** within one tier (a module first on one node, another first on a second), adding 3.0 to 4.2
minutes each.
- **The build is small**: a median of 0.4 minutes for a leaf walk and 0.6 minutes for a core walk of one tier.
- **The rest are sent at once already**: a leaf walk sends the rest a median of 0.0 minutes after its judgement.
The walk ends when the rest are *sent*, not when they run the build. Applying a send takes a median of 10 to
17 seconds per node (the controller's `durations`, seven days, every node), so the measured times are short of
"healthy on every node" by about a quarter of a minute.
- **The merge gate does not build the module.** Its record for a controller pull request on 2026-10-10 shows it
clones the repositories, composes every node's declaration with the change, compiles the controller only to
judge the change with itself (3.2 seconds), and runs the repository's `merge-check.sh` (6 minutes 13 seconds,
before the merge). No artifact is built that the merge could reuse; the merge's build is the first.
Three limits of the measurement: a walk opens only when the walk before it ends, so time beyond the window's
own 1.6 to 2.1 minutes is counted as waiting; the walk keeps only the last judgement pass, so the time of a
first pass is estimated below, not measured; and the check before the merge is not counted, because the
operator's target starts at the merge.
**Checked against GENESIS.** *Anything requiring a human to notice it will be noticed late*: a fix that takes
fifteen minutes to land is noticed by the operator waiting for it. *Failure must be loud*: a delivery over its
budget raises a condition. *Evidence over assertion*: every lever below is weighed by what it would have saved
in the walks above. Nothing here conflicts with GENESIS.
**Words.** The glossary has no word for the time from a merge to its modules running healthy everywhere, nor for
the time it is held to, nor for the two kinds of module. This record proposes **delivery time**, **delivery
budget**, **core module** and **leaf module** for the Change and delivery domain, with this record.
## Considered Options
Each lever is weighed by what it saves in the 20 leaf walks measured, against what it costs.
1. **No merge window for a lone merge** (chosen). Saves the window's 1.8 minutes on every lone merge. The window
exists to put the parts of one change, merged together across repositories, into one walk (ADR 0276). A
merge whose delivery group has no member still unmerged has no part to wait for.
2. **Independent walks in parallel** (chosen). Saves the wait behind another walk: 1.8 to 13.3 minutes in 4 of
the 9 leaf walks since the window went live, 34.7 minutes over the 19 leaf walks outside the issue 362
incident. Its cost is two builds judged on one node at once, which the rules in decision 3 keep apart.
3. **Judgement passes by the module's class** (chosen). One healthy reading takes an estimated minute after the
send (a send applies in 10 to 17 seconds, and the controller acted on a build in a median of 0.3 minutes),
against 2.6 minutes for three. Saves about 1.5 minutes per tier of a leaf walk. The cost is a crash that shows
only after the first reading, which a second reading forty seconds later catches for a module with a
long-running resource. A module whose every resource is a tools bundle has no process of its own to crash: the
tool runner serves its tools or does not.
4. **The first nodes of one tier judged at once** (chosen; not in issue 382, found by the measurement). Saves
3.0 to 4.2 minutes in 5 of the 20 leaf walks.
5. **Reuse the merge gate's build of the pull request's head** when the merged tree equals the checked one.
Rejected: the merge gate builds no artifact today (see Context), so there is nothing to reuse. Building every
head's artifacts at its check, to save the merge's build, would save at most the build's median 0.4 minutes
for a leaf walk, cost a build on every push to a pull request, and need ADR 0238 decision 1 ("anything built to
check it is never registrable") reopened.
6. **A build cache.** Rejected as a lever of this record: the build is 5 % of a leaf walk's time, and the
artifact that did not change is already not rebuilt (ADR 0267's build source).
7. **Sending the rest at once after the first passes.** Already so: the rest are sent a median of 0.0 minutes
after the judgement. Nothing to gain.
8. **The plan says when it is expected to land** (chosen). Saves no time. It tells the operator, while waiting,
when to look, from the same measurements.
9. **One budget for every module, five minutes.** Rejected: a core module's judgement is what protects the
mesh's ability to reach a node at all. A broken node-engine, tool runner, bus or resolver cannot be put back
from outside the node, and the controller judges every other build. The controller's judgement alone takes 3.6
minutes; its three readings are kept.
With options 1 to 4 applied to the 20 measured leaf walks (no window, no wait, one judgement of about a minute
per tier with its first nodes at once, plus a quarter of a minute for the rest to apply), the median would be 2.1
minutes and the worst 5.3; **19 of 20 within five minutes**, against 7 of 20 measured. Keeping three readings
and applying only options 1, 2 and 4 gives a median of 3.9 and a worst of 9.6 (18 of 20).
## Decision
1. **The target, and how it is measured.** A delivery's **delivery time** runs from its merge to the moment
every node of its deploy plan that is heard from runs its modules' builds and reports them healthy. A node not
heard from (a laptop asleep) is listed and not waited for, as ADR 0236 leaves it. **A leaf module's delivery
budget is five minutes. A core module's is ten.** A delivery held for a person (a module that records, a hold,
a bus step) is shown and not held to a budget: its wait is the person's, and the controller's
condition for a walk left waiting (S16) and its probe of stalled deliveries (D14) already speak for it.
- A **core module** is one on the controller's own path (the controller, the node-engine, the tool runner, the
bus, mesh-delivery) or the mesh's resolver. A **leaf module** is every other module. A delivery's class is
that of the slowest-budgeted module it moves.
- The ten minutes for a core module are the measurement's: the 13 core walks of one tier took a median of
5.6 minutes and at worst 8.8. A controller merge that also walks the build agent and the route proxy (median
14.2) meets it once issue 338's path precision is built: that work is tracked by issue 338 and is the
planner's, not this record's.
2. **No merge window for a lone merge.** When a merge arrives, no batch is assembling, and its delivery belongs to
no delivery group with a member not yet merged, the controller cuts its batch at once. A merge of a group still
waiting for members, or a merge arriving while a batch assembles, waits for the window as ADR 0276 decided.
3. **Independent walks run at once.** A batch becomes a walk without waiting for an open walk when it shares with
every open walk no repository, no module, and no edge of the module graph between their modules. Where they
meet, they wait for each other:
- **one first-node judgement per node at a time**: a walk's first node is chosen among its module's nodes
where no other walk is judging; a send to a node where another walk's build is being judged waits for that
verdict, so ADR 0236 decision 4a (no build reaches a node without a judgement) holds as written;
- **a core walk keeps its ground**: no leaf build is sent to a node where a core build is being judged, and
while a new controller is judged, no leaf judgement counts a reading; those resume when it passes or is put
back. A core send never waits for a leaf judgement: a leaf judgement open on that node is set aside without
a verdict and starts again, from its first reading, after the core build's verdict, so a core delivery is
not held past its budget by a leaf one;
- two walks of one repository never run at once (a repository's later merge contains its earlier ones, ADR 0276 decision 2), and a merge into a repository with
an open walk joins that repository's next batch.
This replaces ADR 0276 decision 4's "one walk at a time"; its rule that a walk which started is never
superseded stands.
4. **Judgement by class.** The first-node gate's readings, each at least forty seconds after the one before:
- a **core module**: three, the first at least two minutes after the send, as ADR 0236 decision 2 decided;
- a **leaf module with a long-running resource** (a service, a container, a process the node-engine runs):
two, the first after the node-engine states every such resource healthy (ADR 0240);
- a **leaf module whose every resource is a tools bundle**: one, once the tool runner serves its tools.
Everything else ADR 0236 decided of the first-node gate stands: no witness put the build back, no condition
raised since the send, ten minutes at the most, and a rollback when it fails.
5. **One tier's first nodes are judged at once**, each by its own readings, and the tier passes when every one
has passed.
6. **Every walk keeps its phases, and every delivery its delivery time.**
- The controller records in the walk, and says in `plan-moved`, each phase of the table above with its start
and end, each judgement reading with its time, and the first healthy report of each node of the rest. Its
`durations` gains the kind `walk-phase`, per class.
- mesh-delivery records each delivery's delivery time, its class and its phases from the walk. A new verb,
`times`, answers each class's budget, the last seven days' median and worst delivery time, and every
delivery over its budget with where its time went; `deliveries` heads its list with one line of the same,
and `show` gives a delivery's phases.
- The controller's `status` shows that line, read from `times` by the self-check, beside what is wrong.
7. **A delivery over its budget is loud.** The self-check's probe reads `times` and raises the warning
`delivery.<class>.over-budget` when a delivery's time exceeds its class's budget, naming the delivery and its
longest phase. It clears when the next delivery of that class lands within its budget.
8. **The plan says when it will land.** `plans` and mesh-delivery's `show` give an open batch and an open walk an
expected landing time: now plus the remaining phases at the last seven days' median for its class, and once a
walk is judging, plus the readings still owed.
## Consequences
- **A leaf fix lands within five minutes when nothing else is in its way**, by the measurement's own estimate:
build 0.4, send 0.3, one or two readings at about a minute each, the rest a quarter of a minute.
- **A burst of unrelated merges no longer queues**; it walks in parallel, and one node judges one walk at a time.
The controller holds several open walks, and every reader of a walk (`plans`, the walk's conditions, `plan-moved`,
mesh-delivery) reads a list where it read one.
- **A leaf module is judged on less evidence.** A build that fails after its last reading reaches the rest. That
is bounded: the rest's first healthy report is recorded, a failure there is a condition on that node, and the
build that broke it is put back the ordinary way. The trade is the operator's stated choice of speed for leaf
modules; core modules keep every reading.
- **A core walk still holds its ground.** A leaf delivery whose node is judging a core build waits up to that
judgement (3.6 minutes for the controller) and may exceed its budget; the condition says so, and the week's
numbers show how often.
- **The core budget depends on issue 338.** Until the planner moves only the modules whose build source a merge
touched, a controller merge exceeds ten minutes and raises the condition each time. That is intended: the
condition counts what issue 338 costs.
- **What got harder:** the walk's record grows by the phases and readings, kept in the controller's store as a
numbered migration; the scheduler of walks gains the independence test and the per-node judgement rule, each a
place where two walks can disagree, and each tested.
- **On acceptance**, ADR 0236 (decision 2, three readings for every module) and ADR 0276 (decision 4, one walk at
a time; decision 1, the window for every merge) each gain a note saying the mechanism moved here, and to-be 47
lists this record among its decisions.
## How it is checked
| Rule | Checked by |
|---|---|
| a lone merge is cut at once; a group's member and a merge during assembly wait | the controller's tests: a lone merge asks its build with no window; a merge whose group has an unmerged member waits for the window; a second merge within the window joins the batch |
| independent walks run at once, dependent ones wait | the controller's tests: two catalogue merges of unrelated modules walk at once; two of one repository, two sharing a module, and two joined by a graph edge do not; two walks never judge on one node at once; a send to a node under another walk's judgement waits for its verdict; no leaf reading counts while a new controller is judged |
| judgement by class | the controller's tests: a tools-only leaf passes on one reading, a leaf with a service on two forty seconds apart, a core module on three two minutes after the send; each fails and is put back as before |
| one tier's first nodes at once | the controller's test: a tier whose modules are first on two nodes judges both at once and passes when both pass |
| every phase and the delivery time are recorded | the controller's test of the walk record and `plan-moved`; mesh-delivery's tests of the delivery time, its class, `times` and `show` |
| a delivery over budget is loud | the probe's test: a leaf delivery of 5 minutes 10 seconds raises `delivery.leaf.over-budget` naming its longest phase; the next within budget clears it |
| the target holds on the live mesh | `mesh-delivery.times`: the week's leaf median within five minutes and the condition raised by no more than one leaf delivery in ten; the same measurement as this record's Context, repeated a week after the last part is built |
## References
- Issue 382 (a small fix takes 45 minutes to reach a node) and issue 338 (a module built from a shared repository
moves on every merge to it), in the issue tracker.
- [ADR 0236](0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md):
the first-node gate's three readings, kept for core modules.
- [ADR 0238](0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md):
a check's build is never registrable, which option 5 would have reopened.
- [ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md):
mesh-delivery owns the delivery, and so its delivery time.
- [ADR 0267](0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md):
an unchanged artifact is not rebuilt.
- [ADR 0276](0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md):
the merge window and the walk this record changes in part.
- [Research 044](../01-RESEARCH/044-what-leaves-the-controller-next/00-overview.md), whose question 6 asks for
the time from merge to a node running the fix as a baseline; the table above is that baseline.
- To-be [47](../03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md), amended with this record.
@@ -0,0 +1,187 @@
---
topic: how we work
status: accepted
date: 2026-10-11
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md
---
# 290. A decision record lives whole in the knowledge base, made of clauses
> **Rewritten — 2026-10-11, on the operator's word.** This record was merged an hour earlier as *The knowledge base
> holds each record's structure and clauses, and hq keeps its reasoning*: the store held the structure, and this
> repository the reasoning, reviewed in pull requests. The operator asked why one record should live in two places,
> and asked for this record to be updated in place rather than superseded. Nothing had been built on the earlier
> version. It is in this repository's history at commit `08b1077b`.
> **Updated — 2026-10-11, on the operator's word.** Rule 9 left open whether this repository keeps a generated
> public copy of accepted decision records and research efforts (issue 435). The operator answered: no copy is needed. Rule 9 and the consequences
> say so.
## Context
[Research 045](../01-RESEARCH/045-a-knowledge-base-the-mesh-writes/00-overview.md) measured, on 2026-10-11, how an
agent learns what the mesh decided ([findings](../01-RESEARCH/045-a-knowledge-base-the-mesh-writes/01-findings.md)).
The operator gave the purpose the same day: the decisions and research should become shared knowledge that agents
reach through tools, instead of reading markdown files, and a session should no longer have to start in this
repository's folder to know what was decided.
**An answer costs too much.** "What holds about the merge window" took 26 search hits in 11 documents and about 85 KB
of reading, some 21,000 tokens. The decision index, generated by the records module
([ADR 0248](0248-the-decision-index-is-generated-when-it-is-read-and-never-stored.md)), is 77 KB, more than one
tool answer may be. The records module matches a phrase literally and knows nothing of which record still holds
([ADR 0153](0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)).
**What a decision decided is not data.** 226 of 263 decision records write their `Decision` section as prose; 37
write it as numbered statements with a bold lead (198 in all). A later record that changes one statement of an older
one could say so only in prose or by naming the whole record: 21 `supersedes-in-part` links had no reverse, and 6
more records said it in prose alone, until hq pull request 305 declared all of them in both records and added a check.
**The checks run after the fact.** Of the 33 rules the hq checks enforce, 25 judge one record and 4 judge its prose;
all 29 could refuse a bad record when it is written. Only 4 need the whole record set.
**Numbers collide.** 16 decision numbers were taken twice by sessions working at once between 2026-09-25 and
2026-10-10. [ADR 0275](0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md)
rule 8 already decided that the issue tracker hands out decision and research numbers; issue 424 tracks building it.
### Against GENESIS
- **The mesh owns the truth** ([mission](../00-META/mission.md)): a record lives in one place, written through one
module, not in a file every branch may edit.
- **The mesh brokers everything the work needs, knowledge included**: an agent asks the mesh what holds, by
capability, instead of knowing which folder to read.
- **The mesh remembers** ([effect](../00-META/effect.md)): what was decided, and why, is available to whoever needs it
next, on any node, in any folder, with every earlier version kept.
- **Failure must be loud**: a write that would leave the records inconsistent is refused when it is made.
## Considered Options
### Where a record lives
1. **hq stays the truth; a module indexes it** (the research's option A). Rejected: a link about one statement of a
decision stays prose, and every inconsistency is found after it is written.
2. **Divided: the knowledge base holds a record's structure and clauses; hq keeps its reasoning.** Rejected by the
operator on 2026-10-11: one record in two places, an agent sent to a file for the *why*, and the structure written
twice until the frontmatter left hq.
3. **The knowledge base holds a record whole: structure, clauses and reasoning, with every version.** Adopted. It is
the division the operator made for issues ([ADR 0291](0291-an-issue-lives-whole-in-the-issue-tracker.md)), applied
to decisions and research.
### The unit
1. **The record.** Rejected: a later record changes one statement of an older one far more often than the whole.
2. **The clause: one numbered statement a decision makes, with its own status.** Adopted. The research called it a
*rule*; *rule* already names a line of [how-we-build](../00-META/how-we-build.md), and one word names one thing
([ADR 0244](0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md)).
### How a record is reviewed, once it is not a pull request
1. **The operator approves every record.** Rejected: records are reviewed today by an independent agent, and the
operator is asked only for real decisions; making every record wait for one person slows what works.
2. **An independent agent reviews it, the write-time checks pass, and a record that needs the operator waits for the
operator's Approve on a channel that proves who answered.** Adopted. It is how a pull request here is merged
today, and the operator's Approve already reaches the mesh that way
([ADR 0274](0274-an-agent-proposes-a-shared-instruction-section-and-every-node-takes-it-only-on-the-operators-warrant-which-it-proves-itself.md)).
### Who confirms the clauses drafted for the 226 prose decisions (the research's open question 1)
1. **The operator, topic by topic.** Rejected: 226 records is days of reading for one person, for statements already
decided and reviewed once.
2. **Nobody: drafted clauses are served as they are.** Rejected: a summary nobody checked would speak as the record's
word, which is the work-that-reports-success-and-did-nothing fault.
3. **A second judgement, independent of the one that drafted them, confirms each record's clauses against its text;
the operator is told per topic and may send any record back.** Adopted, and confirmed by the operator on
2026-10-11.
## Decision
1. **The knowledge base is a Go module, `mesh-knowledge`, with one instance on the control-node.** It declares and
claims one seat held once for the mesh, `knowledge-base`
([ADR 0126](0126-a-module-declares-its-own-seats.md)). Its first kinds are the **decision record** and the
**research effort**.
2. **The knowledge base is the truth for a record whole**: its number, kind, title, topic, status, date, deciders, its
typed links (held in both directions), its clauses, and its reasoning (context, options weighed, consequences,
how it is checked; a research effort's documents). **Every version is kept**, with who wrote it and when, and any
two versions can be compared. Once the import has run and been checked, no record is written in this repository.
3. **A decision record is made of clauses.** A clause is one numbered statement of what was decided, with a status:
`proposed`, `in force`, `changed` by a clause of a later record, or `superseded`. A link that is about a clause
names the clause, not only the record. A record is written as clauses from the start. Until the knowledge base
takes writes, a record written in this repository from 2026-10-11 on writes its `Decision` section as numbered
clauses with a bold lead, and `records.py` refuses an accepted one that does not.
4. **A record is proposed, reviewed and accepted in the knowledge base.** `propose` writes it with a number from
`reserve`, and refuses it outright when one of the 29 rules that judge one record or its prose fails. An
independent agent reviews it (`review`), and its approval moves the record and its clauses to `accepted` and
`in force`, and every clause it changes or supersedes accordingly. A record that says it needs the operator waits
for the operator's Approve, asked on a channel that proves who answered, and the warrant is kept with it
([ADR 0259](0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md)).
An accepted record changes only by a new record, or by a dated correction of a fact, written as a new version and marked as one;
the 4 rules that judge the whole record set run over it after every acceptance and raise a condition when one
fails.
5. **Numbers come from the issue tracker's counters**, as ADR 0275 rule 8 decided: `reserve` asks the tracker for the
next decision or research number, and a reserved number is spent whether or not its record is accepted. Until the
tracker hands them out, tracked by issue 424, a session takes the highest number on `main` and on every open
branch, plus one, as the shared instruction section *writing-decision-records* says.
6. **An answer is the shape agents need, bounded.** The verbs are `ask` (records and clauses bearing on a question,
ranked, each saying whether it holds or what changed it), `get` (one record: clauses, links both ways, what changed
it since; its reasoning only when asked), `holds` (the clauses in force for a topic or a record), `links`,
`history` (a record's versions and the difference between two), `reserve`, `propose`, `review`, `import` and
`return` (send drafted clauses back to be drafted again). `ask`, `get` and `holds` stay under a size bound they
state; the default is 4 KiB.
7. **State is in a database in the store**, provided to the module, not in buckets: `ask` needs ranked full-text
search, and a record's versions are a record set to search, not current state to watch
([ADR 0201](0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)
is not narrowed). **Every change is an event the module declares**
([ADR 0041](0041-events-are-a-relationship.md)), carrying what changed and who changed it.
8. **The import reads this repository at a commit and can run again.** It writes every decision record and research
effort whole, at its own number, with the commit as its first version's source; it creates what is absent, never
overwrites, never advances a counter, and a second run changes nothing and says so. A record whose clauses are
written as numbered statements gets them as `written`. A prose record gets them `drafted` by the agent's judgement
([ADR 0278](0278-the-issue-tracker-checks-every-new-issue-for-a-duplicate-through-a-judgement-the-agent-makes-without-tools.md)),
then `confirmed` by a second judgement that sees only the record and the drafts; the operator is told per topic and
may send a record back. An answer marks a drafted or confirmed clause as such, and never serves a drafted clause as
in force.
9. **This repository stops holding decision records and research efforts** once the import is checked against it,
record by record: one pull request removes them, rewrites the mentions of their paths to "ADR NNNN" and "research
NNN", and changes playbooks 01 and 02, tracked by issue 426. This repository keeps no copy of them, generated or
otherwise; the knowledge base is the only place a decision record or research effort is read. The records module
stays the literal reader of what this repository still holds.
10. **It is built in slices, each tracked by an issue.** The first is the module for decisions and research: import,
clauses, answers, and proposing and reviewing a record, tracked by issue 425. The records leave this repository
under issue 426. Designs, playbooks and glossary entries follow the same way, tracked by issue 428. Meeting notes,
drawings and blog posts come last and are not decided here, tracked by issue 429. The design is
[to-be 55](../03-DESIGN/01-to-be/55-the-knowledge-base.md).
## Consequences
- An agent on any node asks what holds and why, and gets the clauses in force, the records they come from and what
changed them, in one call, instead of reading several records whole. No session needs this repository's folder to
read or write a decision.
- A later record that changes one statement of an older one says which, and the older clause's status changes when
the later record is accepted.
- Review moves from a pull request to the knowledge base's `review`, and the forge's line comments and diff are
replaced by the record's versions and `history`. A record is checked when it is written, not when it is merged.
- Until the first slice takes writes, records are still written here as pull requests, with numbered clauses, and the
import takes them in with the rest.
- What got harder: one more module the mesh's own process depends on. Reading or writing a decision needs the mesh to
be reachable; a node that cannot reach the control-node says so rather than guessing. Once the records leave it
(issue 426), the public repository no longer shows the mesh's decisions or research.
## How it is checked
- **Clauses in a record written here** (rule 3): `records.py`, check `clauses`, on every pull request of this
repository, until the records leave it.
- **What the module does** (rules 2, 4, 6, 7, 8): the module's tests and its live acceptance, listed in
[to-be 55](../03-DESIGN/01-to-be/55-the-knowledge-base.md) §9, built under issue 425.
- **Nothing lost when the records leave** (rule 9): every record compared with its file at the import's commit before
the files are removed, the result written on issue 426.
- **Numbers** (rule 5): the issue tracker's tests of its counters, built under issue 424; until then the
`numbering` and duplicate checks of `records.py` and `cycle.py` find a collision on `main`.
## References
- [Research 045](../01-RESEARCH/045-a-knowledge-base-the-mesh-writes/00-overview.md) and its
[findings](../01-RESEARCH/045-a-knowledge-base-the-mesh-writes/01-findings.md).
- [To-be 55 — The knowledge base](../03-DESIGN/01-to-be/55-the-knowledge-base.md).
- hq pull request 305: every partial supersession declared in both records, and the `partial-supersession` check.
- Issues 424, 425, 426, 428, 429 and 435 (answered: no copy in this repository).
@@ -0,0 +1,172 @@
# 02-DECISIONS
Architecture decision records — the "why" trail behind the rules in
[`00-META`](../00-META/) and the specifications in [`03-DESIGN`](../03-DESIGN/).
**Numbered `02` because a decision precedes the design it authorises.** Research concludes,
the decision is recorded here, and only then is the design written. Following the folder
numbers walks the process in the order it happens.
One file per decision, numbered, never deleted. A superseded record has its `status:` changed
and gains a pointer to what replaced it — **its reasoning is never rewritten**. The reasoning that
was rejected is the expensive half to rediscover.
## Progressive insight
A record is a decision, not a snapshot of everything that was true the day it was written, and
those two fail differently. **A fact a record asserted can turn out to be wrong while the decision
it supports stays right** — a count taken before anyone measured, a file named that does not
exist, a proof attributed to a step that cannot run it. Superseding a record for that buries a
correct decision under a second one, and teaches every reader to first work out which of two
records is live. Done a few times, the reading order stops being one.
So: **a correction of fact that leaves the decision standing is made in the record, in place,
marked and dated.**
> **Progressive insight — YYYY-MM-DD.** What was found, what the record said before, and what it
> says instead.
Three conditions, all of which hold:
- **It corrects a fact, not a judgement.** That a suite does not exist is a fact. That building it
is the wrong order is a judgement, and judgements supersede.
- **It adds; it never quietly replaces.** Where body text changes, the note says what stood there
before, so a reader who followed a citation to the old wording can find out what happened to it.
A correction nobody can see is indistinguishable from a record that was always right, which is
the failure the immutability rule exists to prevent.
- **The decision, the options weighed and the consequences stand untouched.** If the correction
changes what was decided, which alternatives were rejected, or a consequence another record
relies on, it is not an insight — write the superseding record.
**What still supersedes**, without exception: reversing a decision, changing its scope, rejecting
an option it accepted, or making a consequence false that a later record cites. The test is not
how large the edit looks in a diff; it is whether a reader who acted on the old text would now be
wrong about *what was decided* rather than about *a detail the decision did not rest on*.
**How this is checked.** `00-META/checks/records.py` requires every insight to be marked in the
form above and dated no earlier than the record's own `date:` — an unmarked edit is a rule
violation the reviewer looks for in the diff, and a marked one is legible in the record itself.
The git history is the backstop, not the record of intent; the note is the record of intent.
## A pointer back from what a record changes
A new record naming an old one is not enough. **Where a record changes a mechanism an older record
states — without reversing the decision, so no supersession — the older record gets a dated note
saying where its mechanism now lives.** A reader arrives at the old record by following a citation,
and finds text that is still the decision and no longer the method; nothing in it says a later record
moved the method, and the new record is not in their hands.
> **The mechanism changed — YYYY-MM-DD, by ADR NNNN.** What still stands, what moved,
> and why.
Three examples of the shape, all found by being missed: ADR 0066 still described a routed name being
written into every container after 0148 replaced that with resolution; ADR 0047 still said a module's
code runs in a container after 0150 made it a supervised process; and ADR 0016 still read as though the
lab were the test bed after 0149 said the live mesh is. Each was a citation leading to the wrong
answer, in a record that was not wrong about anything it decided.
**This is not machine-checked, and it cannot be from `extends:` alone.** 102 records extend another and
87 name a parent that does not mention them, which is correct: extending usually means building on a
context, and a one-directional pointer is the right shape for that. What needs a note is the narrower
case where the parent's own text has gone stale, and which case that is, is a judgement — so it is a
rule for the author and the reviewer, and the diff is where it is caught. Making it mechanical would
mean a record declaring the relationship in its frontmatter, which is a change to the record schema and
has not been decided.
**A supersession in part is the declared case, and it is machine-checked.** A record that replaces part of
an older one names it under `supersedes-in-part:`, and the older record carries, directly under its
heading and newest first, a note in the form
> **Superseded in part — YYYY-MM-DD, by ADR NNNN** (a link to it). What no longer holds, and what stands
> as decided here.
dated by the newer record's `date:`. `00-META/checks/records.py` (`partial-supersession`) fails when
either half is missing, when the note's date is not the newer record's, and on a note in the older form
*Narrowed, not replaced*. Where the note sits is the reviewer's to see in the diff. Saying "this narrows" or "this replaces" an older record in prose alone is not
enough: on 2026-10-11, 6 records did only that, and 11 of 21 declared links had no note on the record
they changed.
The records run in the order the decisions were taken, oldest first.
**Every decision is a record.** There is no ledger and no index file — if a decision is worth
recording it is worth a record, and if it is not worth a record it is not recorded
([ADR 0019](0019-how-this-repository-works.md)). A "decision" small enough to be one line is
almost always a **rule**, and a rule belongs in
[`00-META/how-we-build.md`](../00-META/how-we-build.md), where it is enforced and keeps the
incident that earned it.
## Frontmatter
```yaml
---
status: proposed | accepted | superseded
date: YYYY-MM-DD # when the decision was taken, not when it was written down
deciders: name
reconstructed: true|false # true when the record was written after the fact from evidence
superseded-by: # 02-DECISIONS/NNNN-....md, when status is superseded
extends: # 02-DECISIONS/NNNN-....md, when this record widens an earlier one
supersedes: # a list of 02-DECISIONS/NNNN-....md this record replaces whole
supersedes-in-part: # a list of 02-DECISIONS/NNNN-....md of which this record replaces a part
---
```
## Body
```
# N. Title in plain language
## Context what was true, with evidence
## Considered Options numbered, each with why it was rejected
## Decision what was decided
## Consequences what follows, including what got harder
## References commits, pull requests, knowledge-base entries, prior art
```
State evidence, not assertion. *"Zero of 124 modules declare `brain` as a dependency"*
outranks *"the dependency rule is not followed"*.
## Reconstructed records
Records 0001–0014 were written on 2026-08-23, after the decisions they describe. Records 0015 onward were taken as records. Those
decisions were taken in implementation rather than in a document; the records state what was
decided and the evidence it was decided from, and each carries `reconstructed: true` and says
so in its first lines.
A reconstructed record is not a transcript. Where the deliberation is not recoverable, the
options section states what the alternatives were and why the chosen one won on the evidence
available — not a discussion that did not happen. Where a date is not establishable it says so
rather than guessing.
## Reading order
**A number identifies a record and never changes.** Records are referenced from outside this
repository — code comments, commit messages — so a number that moves invalidates them silently.
Renumbering once cost 96 references across two code repositories, and that is why the numbers
are now fixed.
So the folder is in creation order, and **the reading order lives here**: six topics, in the
order somebody would learn the system. Each record names its own in `topic:`.
1. **What the mesh is** — `topic: the mesh`. What it is for, and the parts it is made of.
2. **Its tiers, from the bottom up** — `topic: the tiers`. The foundation, the node and the controller, and
what each one provides to the one above.
3. **What runs on them, and how it gets there** — `topic: what runs on it`. Modules, seats and
provisions, and how they reach a node.
4. **How it is built** — `topic: building it`. The code, its repositories, and what is made from
it for a node.
5. **How it is checked** — `topic: checking it`. What judges a change, and what proves a decision holds.
6. **How we work** — `topic: how we work`. This repository, its playbooks, and how decisions and
designs are made.
**The list of records under each topic is generated when it is read, and never stored here**
([ADR 0248](0248-the-decision-index-is-generated-when-it-is-read-and-never-stored.md)). A stored list
was one more line in this file for every decision, so two decisions open at once always conflicted
([issue 297](../04-ISSUES/297-two-open-decisions-always-conflict/00-report.md)). Read it with:
```
python3 00-META/checks/index.py --print the records, by topic, in reading order
```
or the `hq-status` skill, or the records module's `records_decisions` from anywhere on the mesh.
`index.py` checks that every record's topic is one of the six above, and fails if this file stores the
list again.
@@ -0,0 +1 @@
d1edcf544b384502fded36c1605571ae74b07ed5